Home > Back-end >  Powershell - if statement operator through paramenter
Powershell - if statement operator through paramenter

Time:07-19

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 to Invoke-Expressions are available, as in this case: see the solution below, and this answer for background information.
  • In your case specifically, embedding $varA and $varB as-is inside an expandable (double-quoted) string ("...") means that evaluation via Invoke-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.
    • 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 as ). As Santiago Squarzon points out, you can use System.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 the System.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

}
  • Related