Hello Powershell expert, i have a short question regarding a powershell issue of mine.
I try to pass parameter variables to process these variables in an if statement.My Goal is to have a fully dynamic if statement. Let me show you the circumstance:
function Get-Test {
param(
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$varA,
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$varB,
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$op
)
$statement = "$varA $op $varB"
if ($statement) {
Write-Host "One"
} else {
Write-Host "Two"
}
}
Get-Test -varA "Test1" -varB "Test1" -op "-ne"
Explanation: No matter what i put in as the paramter $op it is always gonna get to "One"
So my questions is: Is there any possibillity to use parameters/varibales to have a sort of dynamic operator in my if statement ?
CodePudding user response:
Per @SantiagoSquarzon's comments, one way to do this is to use Invoke-Expression
to generate a string containing a PowerShell command, and then execute it:
function Get-Test {
param(
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$varA,
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$varB,
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$op
)
$statement = IEX "'$varA' $op '$varB'"
if ($statement) {
Write-Host "One"
} else {
Write-Host "Two"
}
}
Get-Test -varA "Test1" -varB "Test1" -op "-ne"
However, you should be very careful to validate your input (especially if it's an "untrusted" source like a user interface) as it contains the same type of problem as a SQL Injection Attack - that is, you could end up running arbitrary code inside your function.
For example if $op
somehow ends up with the value "; write-host 'i didn''t mean to run this' ;"
(e.g. from unsanitised user input or a spiked input file) your function will execute the write-host
command without any errors so your output will look like this:
i didn't mean to run this
One
That might not be so bad in itself, but what if there was something more malicious in the string - e.g. "; Format-Volume -DriveLetter C ;"
- do you really want to be executing that command on your server?
One way to address this is to have a list of known operations you'll support - it's a bit more work up front, but it'll avoid the security issue with Invoke-Expression
:
function Get-Test {
param(
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$varA,
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$varB,
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]$op
)
$result = switch( $op )
{
"-eq" { $varA -eq $varB }
"-lt" { $varA -lt $varB }
"-gt" { $varA -gt $varB }
# etc for any other operations you want to support
default {
throw "invalid operation '$op'"
}
}
if ($result) {
Write-Host "One"
} else {
Write-Host "Two"
}
}
If you try that with an invalid operation you'll get something like this:
PS> Get-Test -varA "Test1" -varB "Test1" -op "; write-host 'i didn't mean to run this' ;"
Exception:
Line |
19 | throw "invalid operation '$op"
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| invalid operation '; write-host 'i didn't mean to run this' ;
CodePudding user response:
As for what you tried:
Your function makes no attempt to evaluate the value of expandable string
"$varA $op $varB"
as code.Given that any non-empty string in PowerShell is considered
$true
in a Boolean context,if ($statement)
is therefore always true.
While Invoke-Expression
allows you to evaluate a string as PowerShell code, it comes with important caveats:
In general, it makes your code vulnerable to injection of unwanted commands (see mclayton's comment on the question for an example).
- In short: Only ever use
Invoke-Expression
if you either fully control the input string or implicitly trust one that you have been given. Typically, superior and safer alternatives toInvoke-Expressions
are available, as in this case: see the solution below, and this answer for background information.
- In short: Only ever use
In your case specifically, embedding
$varA
and$varB
as-is inside an expandable (double-quoted) string ("..."
) means that evaluation viaInvoke-Expression
would fail for string parameter values and, generally, for values of .NET types that do not have meaningful string representations.
Therefore, do the following:
To prevent code injection, use PowerShell's language parser to examine
$op
to ensure that it truly represents a PowerShell operator.Alternatively:
- Use regex matching to ensure that
$op
represents a (potential) operator, such as$op -match '^(-[a-z]{2,}|[- */%])$'
- Match against an explicit set of allowable operators, as shown in mclayton's helpful answer.
- Use regex matching to ensure that
Note: The solution below uses
System.Management.Automation.PSParser.Tokenize
, which only tells you whether a token represents an operator in the abstract, irrespective of category (comparison, logical, ...), arity (unary, binary, ternary), or syntax form (-
-prefixed or symbol-based, such asSystem.Management.Automation.Language.Parser.ParseInput
instead, which provides more detailed operator information, but requires more effort; e.g., the following determines whether$op
represents a binary operator, irrespective of category; if you wanted to limit operators to binary comparison operators, use'BinaryPrecedenceComparison'
instead; see theSystem.Management.Automation.Language.TokenKind
enumeration:$op = '-ne' # sample input $tokens = $null # Note: '-not' is a dummy operator that forces $op to be parsed in expression mode. $null = [System.Management.Automation.Language.Parser]::ParseInput( "-not $op", [ref] $tokens, [ref] $null ) # Exactly 3 tokens are expected: # 1 for (dummy token) -not, one for $op, and the EndOfInput token. [bool] ($tokens.Count -eq 3 -and ($tokens[1].TokenFlags -band 'BinaryOperator'))
Instead of using
Invoke-Expression
, construct a script block from an expandable string, and pass$varA
and$varB
as arguments to that script block, which ensures that their values are used as-is (rather than undergoing stringification).
Solution:
function Get-Test {
param(
[parameter(Mandatory)]
[ValidateNotNullOrEmpty()]$varA,
[parameter(Mandatory)]
[ValidateNotNullOrEmpty()]$varB,
[parameter(Mandatory)]
[ValidateNotNullOrEmpty()]$op
)
# IMPORTANT: To prevent code injection, ensure that $op really refers to
# a PowerShell operator.
# Note: '-not' is a dummy operator that forces $op to be parsed in expression mode.
$tokenKind = (
[System.Management.Automation.PSParser]::Tokenize("-not $op", [ref] $null) |
Select-Object -Skip 1
).Type
if ('Operator' -ne $tokenKind) {
Throw "'$op' is not a PowerShell operator; it is a token of type $tokenKind"
}
# Create a script block based on the operator, and pass the operands (parameters)
# *as arguments* to the script block.
& ([scriptblock]::Create("`$args[0] $op `$args[1]")) $varA $varB
}