Search code examples
powershellinvoke-command

PowerShell safe expansion of non printing characters in arbitrary strings


I have data in an XML file, that will eventually be used as a Registry path, which MAY contain non printing characters (for example when copying the path from a web site into the XML). I want to validate the data and throw a specific error if non printing characters are found.

In Powershell, if I define a variable with non printing characters in single quotes and then test-Path it tests as a valid path as the non printing character is handled as a literal.

Test-Path 'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`[email protected]/GENUINE\@microsoft.com/GENUINE' -isValid

The same thing with double quotes will "expand" the non printing characters and return false, which is what I need.

Test-Path "HKEY_LOCAL_MACHINE\SOFTWARE\Test\`[email protected]/GENUINE\@microsoft.com/GENUINE" -isValid

I have found reference to [string]::Format(() being used to expand the non printing characters, but

$invalidPath = 'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`[email protected]/GENUINE\@microsoft.com/GENUINE'
[string]::Format("{0}",$invalidPath)

does not expand the non printing character as expected. I have also seen reference to using Invoke-Expression but that is NOT safe, and not an option.

Finally I found $ExecutionContext.InvokeCommand.ExpandString(), which seems to work,

$ExecutionContext.InvokeCommand.ExpandString('HKEY_LOCAL_MACHINE\SOFTWARE\Test\`[email protected]/GENUINE\@microsoft.com/GENUINE')

returns a multiline string to the console, while $ExecutionContext.InvokeCommand.ExpandString('Write-Host "Screwed"') returns the actual string to the console, rather than actually executing the Write-Host and only returning Screwed to the console.

Finally,

$invalidPath = 'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`[email protected]/GENUINE\@microsoft.com/GENUINE'
Test-Path ($ExecutionContext.InvokeCommand.ExpandString($invalidPath)) -isValid

returns false as expected. Which has me thinking this is the correct approach to pursue, but given all the gotchas elsewhere, I want to be 100% sure there is no way for this approach to be used as a security weak point. Am I on the right track, or are there gotchas my Google-Fu hasn't turned up yet?


Solution

  • Like Invoke-Expression, $ExecutionContext.InvokeCommand.ExpandString() is vulnerable to injection of unwanted commands, except that in the latter case such commands are only recognized if enclosed in $(...), the subexpression operator, as that is the only way to embed commands in expandable strings (which the method's argument is interpreted as).

    For instance:

    $ExecutionContext.InvokeCommand.ExpandString('a $(Write-Host -Fore Red Injected!) b')
    

    A simple way to prevent this is to categorically treat all embedded $ chars. verbatim, by escaping them with `:

    'a $(Write-Host -Fore Red Injected!) b',
    'There''s no place like $HOME',
    'Too `$(Get-Date) clever by half' |
      ForEach-Object {
        $ExecutionContext.InvokeCommand.ExpandString(($_ -replace '(`*)\$', '$1$1`$$'))
      }
    

    Note: It is sufficient to escape verbatim $ in the input string. A Unicode escape-sequence representation of $ (or ( / )) (`u{24} (or `u{28} / `u{29}), supported in PowerShell (Core) v6+ only), is not a concern, because PowerShell treats the resulting characters verbatim.


    Of course, you may choose to report an error if there's a risk of command (or variable-value) injection, which can be as simple as:

    $path = 'There''s no place like $HOME'
    
    if ($path -match '\$') { Throw 'Unsupported characters in path.' }
    

    However, this also prevents legitimate use of a verbatim $ in paths.


    Taking a step back:

    You state that the paths may have been copy-pasted from a web site. Such a pasted string may indeed contain (perhaps hidden) control characters, but they would be contained verbatim rather than as PowerShell_escape sequences.

    As such, it may be sufficient to test for / quietly remove control characters from the string's literal content (before calling Test-Path -IsValid):

    # Test for control characters.
    if ($path -match '\p{C}') { throw 'Path contains control characters.' }
    
    # Quietly remove them.
    $sanitizedPath = $path -replace '\p{C}'