3

I am using Powershell to request a password from a user if not provided, based upon another answer. I then pass the password (no pun intended) to some program, do-something.exe. Rather than have an intermediate variable, I tried to convert the password to a normal string "inline":

[CmdletBinding()]
Param(
  [Parameter(Mandatory, HelpMessage="password?")] [SecureString]$password
)
do-something password=${[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))}

That doesn't work. I could only get it to work using a temporary, intermediate variable:

[CmdletBinding()]
Param(
  [Parameter(Mandatory, HelpMessage="password?")] [SecureString]$password
)
$pwd=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
do-something.exe password=$pwd

Did I make a mistake trying to evaluate the password inline when invoking do-something.exe? How can this be done?

Garret Wilson
  • 18,219
  • 30
  • 144
  • 272
  • 1
    instead of `dosomething password=${ }` use `dosomething password=( )`. Also `[System.Net.NetworkCredential]::new('', $pwd).Password` is a lot easier than Marshal. – Santiago Squarzon Oct 26 '22 at 02:15
  • 1
    "… the inline code uses `password=...`". Indeed, that was the problem. I copied the solution which had been written based upon my example, not on my original code. My mistake. "[T]his has turned into one big mess of a question." That it has. The original question was focused on value interpolation (which you answered with a great explanation—thank you), but then we got off into a discussion on Powershell's handling of secure strings, and _that_ is a `$(mess * 2)`. I'll tidy up the question. – Garret Wilson Oct 27 '22 at 03:44
  • I guess I meant `$($mess * 2)`. (Sigh.) It will take me a while to get the hang of this, I think. – Garret Wilson Oct 27 '22 at 03:53

1 Answers1

2

${...} is a variable reference, and whatever ... is is taken verbatim as a variable name.

  • Enclosing a vairable name in {...} is typically not necessary, but is required in two cases: (a) if a variable name contains special characters and/or (b) in the context of an expandable string ("..."), to disambiguate the variable name from subsequent characters - see this answer

In order to embed an expression or command as part of an argument, use $(...), the subexpression operator, and preferably enclose the entire argument in "..." - that way, the entire argument is unambiguously passed as a single argument, whereas an unquoted token that starts with a $(...) subexpression would be passed as (at least) two arguments (see this answer).

  • If an expression or command by itself forms an argument, (...), the grouping operator is sufficient and usually preferable - see this answer

Therefore:

[CmdletBinding()]
param(
  [Parameter(Mandatory, HelpMessage="password?")]
  [SecureString] $password
)

# Note the use of $(...) and the enclosure of the whole argument in "..."
do-something "password=$([Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)))"

Also note:

  • On Windows it doesn't make a difference (and on Unix [securestring] instances offer virtually no protection and should be avoided altogether), but it should be [Runtime.InteropServices.Marshal]::PtrToStringBSTR(), not [Runtime.InteropServices.Marshal]::PtrToStringAuto()

  • As Santiago Squarzon points out, there is an easier way to convert a SecureString instance to its plain-text equivalent (which should generally be avoided[1], however, and, more fundamentally, use of [securestring] in new projects is discouraged[2]):

    [pscredential]::new('unused', $password).GetNetworkCredential().Password
    

[1] A plain-text representation of a password stored in a .NET string lingers in memory for an unspecified time that you cannot control. More specifically, if it is part of a process command line, as in your case, it can be discovered that way. Of course, if the CLI you're targeting offers no way to authenticate other than with a plain-text password, you have no other choice.

[2] See this answer, which links to this .NET platform-compatibility recommendation.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Can you elaborate on why all these things should be avoided (especially the "easier way")? In my case I simply want to 1) prevent the password from appearing in my command history, 2) prevent the password from appearing on the screen, and 3) prevent the password from appearing in the terminal window title. Generally from the *nix world I just don't want to be passing a password on the command line, as historically I think it was possible to see other users' commands (although I don't know if that is still the case). – Garret Wilson Oct 26 '22 at 03:41
  • A little-off-topic follow-up: I also have a param `$path` of type `[System.IO.FileInfo]`. When I invoke `do-something.exe` and pass the path as the last argument, do I need to quote it some way? Currently I'm just passing `do-something.exe --password=$password $path` and it's working fine, even with filenames containing spaces, but I don't understand why that works (unless `do-something.exe` somehow just takes everything after the last parameter as the filename). This relates to your comment about quoting the entire password arg. I still haven't got a feel for when Powershell requires quoting. – Garret Wilson Oct 26 '22 at 03:49
  • 1
    @Garret Wilson, the point is that the [`SecureString`](https://learn.microsoft.com/dotnet/api/system.security.securestring) class should be avoided as a ***secure** string*, that's why I came up with a [`HiddenString`](https://github.com/iRon7/HiddenString) idea some time ago which basically does the same (besides easier conversions and warnings) but better covers the load with its name: [security through obscurity](https://en.wikipedia.org/wiki/Security_through_obscurity). In general, you should avoid passing passwords in your scripts, instead authenticate the account that runs the script. – iRon Oct 26 '22 at 07:38
  • @GarretWilson, re `[securestring]`: please see my update (footnotes). As for using variables (in isolation) as arguments: They _never_ need double-quoting in PowerShell, irrespective of quoting. If the target command is an _external program_ and the variable doesn't already contain a _string_, its value is stringified, essentially with `.ToString()` – mklement0 Oct 26 '22 at 13:16
  • @GarretWilson, with respect to `[System.IO.FileInfo]`, specifically, there's a _Windows PowerShell_ pitfall (no longer a problem in PowerShell (Core) 7+): they _situationally_ stringify to the file _name_ only, depending on the specifics of the `Get-ChildItem` command that obtained them; the safe approach is to use the `.FullName` property - see [this answer](https://stackoverflow.com/a/53400031/45375). – mklement0 Oct 26 '22 at 13:28
  • "In general, you should avoid passing passwords in your scripts, instead authenticate the account that runs the script." @iRon, right, but my use case is simply removing a password from a PDF (that I have the rights to) using [qpdf](https://qpdf.sourceforge.io/), so for this use case my approach is appropriate and required. I have to pass a password to the application. I just don't want to show everyone, and I don't want it in the command history. – Garret Wilson Oct 26 '22 at 20:25
  • @GarretWilson, you should `@`-mention @iRon, otherwise he may not get notified of your response. – mklement0 Oct 26 '22 at 20:27
  • I don't know why, but `@` mentioning doesn't seem to work for me often on Stack Overflow. Sometimes the site removes the mention altogether, and I never see any links. I don't know if this person gets notified. – Garret Wilson Oct 26 '22 at 20:29
  • There's a lot of great information here. I have to catch up on other work, and try to swing by and digest it all later on. – Garret Wilson Oct 26 '22 at 20:29
  • @GarretWilson, `@`-mentioning is tricky business, and I wish it were clearer who gets notified when. An `@`-mention _disappearing_ usually means that the site considers it redundant, i.e. that the specified user is notified by default anyway. – mklement0 Oct 26 '22 at 20:31
  • I'm circling back. So much more confusing information; I think it's getting less clearer. All the experts said "Don't use `Read-Host`", "Use `Param`, and "Use `[SecureString]`". Now other experts are saying "Don't use `Param`", "Don't use `[SecureString]`", and "Use `Read-Host`." And I'll have to do half an hour of more research just to undersand what `[pscredential]::new('unused', $password).GetNetworkCredential().Password` is doing! – Garret Wilson Oct 27 '22 at 03:03
  • Look, I just want 1) to ask the user for a password, 2) I don't want the password echoed, 3) I want to invoke a command, passing the password as an argument, and 4) I don't want the password to show up in command history, in the system processes, the window, or anywhere else. These are not outlandish requirements. Is this really such a difficult thing for Powershell to handle? – Garret Wilson Oct 27 '22 at 03:05
  • The answer contains a working solution for what you want, and it explains the problem with your own attempt. Does it not work for you? The ancillary information provides general guidance, not just to you, but also to future readers. This gives everyone the chance to decide what tradeoffs to accept. You don't need to understand what `[pscredential]::new('unused', $password).GetNetworkCredential().Password`, you just need to know that it converts a `[securestring]` instance to its plain-text equivalent, just like the - even more obscure - expression in your own attempt does. – mklement0 Oct 27 '22 at 03:14
  • Oh, and when I try `"password=$([Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)))"` and then stick `$path` on the end (containing `foo bar.dat`, the program says, "Unknown argument foo bar.dat". But if I go back to the `do-something.exe password=$pwd $path` version given in the description (with `$path`) on the end, it work just fine. Why? I have no idea. So still this inline evaluation of the password isn't working. – Garret Wilson Oct 27 '22 at 03:14
  • `"password=$([Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)))"` is ONE argument; of course, OTHER arguments must be passed SEPARATELY. – mklement0 Oct 27 '22 at 03:16
  • "Does it not work for you?" No, it does not work for me. "You don't need to understand what …". Yes, I do need to understand what it means. That's just the sort of person I am. – Garret Wilson Oct 27 '22 at 03:16
  • 1
    I should have realized sooner that the `[securestring]` discussion was ancillary and would be better discussed in a separate question. Getting back to this question, @mkelment0 your answer relating to the interpolation was just what I needed. I had gotten confused because languages such as JavaScript use a single interpolation quoting, e.g. `the answer is ${foo +bar()}`, while in PowerShell it appears we need different delimiters for variables and for expressions, i.e. `the answer is $(${foo} + bar())` (adding curly brackets to make the point). But it's sinking in. Thank you. – Garret Wilson Oct 27 '22 at 03:50
  • @Garret Wilson, "simply removing a password from a PDF", I understand that your use case is a pragmatic workaround to the design of the `qpdf` application but I stick with the statement that the [SecureString shouldn't be used](https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md). The correct implementation of the `qpdf` application should be that the *application* prompts for a password. Just like the fact that e.g. the [`runas` command does not allow a password on its command line](https://stackoverflow.com/a/16116329/1701026). – iRon Oct 27 '22 at 06:16
  • @iRon "The correct implementation of the `qpdf` application should be that the _application_ prompts for a password." I don't disagree. But I need passwords removed from PDFs. What do I do? I don't have time to rewrite _everybody's_ software, although I wish I did. – Garret Wilson Oct 27 '22 at 13:07