PowerShell safe expansion of non printing characters in arbitrary strings

Viewed 137

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\`n@microsoft.com/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\`n@microsoft.com/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\`n@microsoft.com/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\`n@microsoft.com/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\`n@microsoft.com/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?

2 Answers

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}'

I was initially running PS Version 7.1.3, in which all of these methods bore the same results.

When I ran:

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

True is returned

Same as:

Test-Path ([regex]::Escape($invalidPath)) -IsValid

As well as the other methods you mentioned.

In testing in 5.1 I saw the same results as you.

In your XML, would you be seeing something like `n or \n or the actual non-printing characters? IE

'HKEY_LOCAL_MACHINE\SOFTWARE\Test\`n@microsoft.com/GENUINE\@microsoft.com/GENUINE'

OR

'HKEY_LOCAL_MACHINE\SOFTWARE\Test\
@microsoft.com/GENUINE\@microsoft.com/GENUINE'

If the latter is true you should be able to just pass the string to test-path in a variable like Test-Path "$invalidPath" -IsValid to get what you're looking for. In 7.1.3 (maybe earlier) it seems that PS is smart enough to parse those escape sequences and such -- or there is no simple way of doing what you're looking for that I can find.

Related