Home > Back-end >  Unable to evaluate Powershell password via interpolation
Unable to evaluate Powershell password via interpolation

Time:10-27

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?

CodePudding user response:

${...} 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.

  • Related