3

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?

Gordon
  • 6,257
  • 6
  • 36
  • 89

2 Answers2

3

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}'
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 2
    I was afraid I was missing some trick there. The fact that it's got `InvokeCommand` right there as a method had me concerned. I might even use a regex to see if there is anything matching $(...) and throw a specific error there. – Gordon Jul 24 '21 at 15:10
  • @Gordon Thats a good idea, and a great way to add a layer of additional security. – rbleattler Jul 24 '21 at 15:41
  • 1
    @gordon - if you’re planning on using regex to parse your input for $(), watch out for Unicode escape sequences - e.g. ```$x = "xxx"; $y = "aaa$`u{28}`$x)bbb"; $ExecutionContext.InvokeCommand.ExpandString($y)```. You may want to use powershell’s built-in parser to pre-process the input string first - see https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.language.parser.parseinput?view=powershellsdk-7.0.0#System_Management_Automation_Language_Parser_ParseInput_System_String_System_Management_Automation_Language_Token____System_Management_Automation_Language_ParseError____ – mclayton Jul 24 '21 at 16:20
  • @mclayton, what Unicode escapes (v6+) expand to is always used _verbatim_, so escaping _verbatim_ `$`, as shown in the answer, is sufficient. – mklement0 Jul 24 '21 at 18:19
  • 1
    @mklement0 - you’re right, of course. That’ll teach me to try to write / run tests on TryItOnline (https://tio.run) using my phone :-). I wasn’t testing what I *thought* I was testing… – mclayton Jul 24 '21 at 18:46
0

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.

rbleattler
  • 334
  • 2
  • 11
  • 1
    Interesting. I just tried with a here string, and I do get a failure. But it would be nice to provide an error code that explains why it failed, so the user can know what to correct. Always been the weak point of something like Test-Path for me. – Gordon Jul 24 '21 at 17:31