Jonathan Eckman
Jonathan Eckman

Reputation: 2077

PowerShell splatting SecureString is converted to String

param
(
    [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$True)]
    [securestring]
    $securityKey
)

powershell.exe -File $PSCommandPath -thisAppDomain @PSBoundParameters

This code throws the following error:

Cannot process argument transformation on parameter 'securityKey'. Cannot convert the "System.Security.SecureString" value of type "System.String" to type "System.Security.SecureString"

If I check the type of securityKey, its is a SecureString before its splatted. I assume its serializing it for some reason. How do I prevent this?

Edit

This use case may look strange so Ill provide some context. I need to ensure a specific version of an assembly is loaded when piping into this script. I'm using the thisAppDomain param to relaunch in a new app domain to attempt to accomplish this. Larger example:

.\FirstScript.ps1 | .\SecondScript.ps1

The secure string is piping in as expected, but is converted to a string when relaunching. This is how I relaunch:

if(-not $thisAppDomain)
{
    Write-Host "Invoking script in a new app domain"
    powershell.exe -File $PSCommandPath -thisAppDomain @PSBoundParameters
    return;
}

Upvotes: 1

Views: 668

Answers (2)

mklement0
mklement0

Reputation: 440297

If you call PowerShell's CLI with -File, all arguments are invariably converted to strings, so your secure string won't work (it is converted to a regular string with literal contents System.Security.SecureString).

In order to (mostly) preserve argument types, you must pass a script block, which causes PowerShell to automatically serialize and deserialize arguments in a type-preserving way[1], which preserves [securestring] instances correctly, but note that this technique only works when calling from PowerShell (too):

if (-not $thisAppDomain)
{
  Write-Host "Invoking script in a new app domain"
  powershell -Command { 
      $script, $splat = $Args # parse the args array into distinct variables
      . $script -thisAppDomain @splat # call the script with splatting
    } -args $PSCommandPath, $PSBoundParameters
  return
}

Note how the values from the caller's context that must be referenced in the script block - which will be executed in the new session - must be passed as arguments, via -args <arg-array> - see Get-Help about_powershell.exe or run powershell -? for more concise help.

Note: As Patrick Meinecke points out, the above approach, which uses a transformed command line with a Base64-encoded string behind the scenes, can hypothetically fail due to exceeding the max. command-line length, which is 32,768 characters in Windows 10 (see the CreateProcess WinAPI function).
That said, unless you pass exceptionally large script blocks and/or hundreds of arguments, you're unlikely to run into this limit.


[1] The same type of serialization is also used in PowerShell remoting and with background jobs, for instance. Note that not only a fixed set of types can be deserialized faithfully, but PowerShell does its best to emulate other types with custom objects that have the same properties.

Upvotes: 1

Patrick Meinecke
Patrick Meinecke

Reputation: 4183

Splatted arguments on an executable (even powershell.exe) are just converted to a string. You can see how it would end up by running this:

$splat = @{ Test = 'something'; Secret = [securestring]::new() }
cmd.exe /c echo @splat
# returns
# -Test:something -Secret:System.Security.SecureString

You can run something in another process while using the SecureString by using Start-Job

$mySecureString = Read-Host -AsSecureString
$job = Start-Job {
    & $using:PSCommandPath -thisAppDomain $using:mySecureString
}

$job | Receive-Job -Wait

Note: the using scope must be used for variables from the parent session. Also, variables will still be serialized since processes do not share memory, but the secure string will be serialized correctly.

Upvotes: 1

Related Questions