PowerShell Invoke-Expression: Run Dynamic Commands Safely

PowerShell Invoke-Expression: Run Dynamic Commands Safely

PowerShell Tips Editor 1 min read
PowerShell Invoke-Expression: Run Dynamic Commands Safely

Invoke-Expression executes a string as a PowerShell command, making it one of the most powerful and most dangerous features in the language. Powerful because it enables truly dynamic command construction; dangerous because any unvalidated input in an Invoke-Expression call is a code injection vulnerability. This post covers how PowerShell Invoke-Expression works, demonstrates safer alternatives that cover most real use cases, and identifies the narrow scenarios where it is genuinely justified.

Basic Invoke-Expression Usage

Invoke-Expression (alias iex) takes a string and executes it as PowerShell code in the current scope. The output is the same as if you had typed the string directly at the prompt:

$command = "Get-Process | Select-Object -First 3 Name, CPU"
Invoke-Expression $command

# Equivalent to typing:
Get-Process | Select-Object -First 3 Name, CPU

# The result is available as return value
$result = Invoke-Expression "Get-Date -Format 'yyyy-MM-dd'"
Write-Host "Today: $result"
Today: 2026-05-04

Dynamic Command Building

A common motivation for Invoke-Expression is building a command string dynamically based on runtime conditions:

# Building a cmdlet call dynamically
$cmdletName = "Get-Process"
$paramName  = "Name"
$paramValue = "svchost"

$dynamicCmd = "$cmdletName -$paramName '$paramValue'"
Invoke-Expression $dynamicCmd

# This seems flexible but creates security risk if $paramValue comes from user input

Security Risks and Injection

If any part of the string passed to Invoke-Expression comes from user input, an attacker can inject arbitrary code. This is a serious vulnerability in any script that accepts external input:

# DANGEROUS — never do this
function Search-Log {
    param([string]$SearchTerm)
    # If $SearchTerm = "x'; Remove-Item C:\* -Recurse -Force; #"
    # the IEX call executes the injected code
    Invoke-Expression "Select-String -Path 'C:\Logs\app.log' -Pattern '$SearchTerm'"
}

# Safe alternative: pass the variable directly, not via string construction
function Search-Log {
    param([string]$SearchTerm)
    Select-String -Path "C:\Logs\app.log" -Pattern $SearchTerm
}

Safer Alternative: Script Blocks

Script blocks ({ }) are pre-compiled PowerShell code objects that can be invoked with the & call operator. They are faster, type-safe, and do not have injection risk:

$action = { param($ProcessName) Get-Process -Name $ProcessName }

# Invoke with &
& $action "svchost"

# Script blocks can be stored, passed, and composed safely
$blocks = @(
    { Get-Date },
    { Get-Process | Measure-Object },
    { Get-Service | Where-Object Status -eq Stopped }
)

foreach ($block in $blocks) {
    & $block | Out-Null
}

Safer Alternative: & Call Operator

The & call operator invokes a command by name stored in a variable. It handles cmdlets, functions, executables, and script paths without string interpolation risks:

$cmdletName = "Get-Process"
$argName    = "svchost"

# Safe — uses & with a variable, not string construction
& $cmdletName -Name $argName

# For external executables
$exePath = "C:\Tools\myapp.exe"
& $exePath --config "C:\Config\app.json" --verbose

# For script files
$scriptPath = "C:\Scripts\Deploy.ps1"
& $scriptPath -Environment "Production"

When IEX Is Justified

Legitimate uses for Invoke-Expression are rare. The most defensible scenarios involve code that is itself generated by trusted tooling (not user input) and cannot be expressed as a script block or call operator:

# Legitimate use: bootstrapping from a trusted internal source
# (This is still best replaced with a local module in production)
$setupScript = Invoke-RestMethod "https://internal-tools.corp.com/setup.ps1"
# Verify checksum/signature before executing
$expectedHash = "ABCD1234..."
$actualHash   = [System.Security.Cryptography.SHA256]::Create().ComputeHash(
    [System.Text.Encoding]::UTF8.GetBytes($setupScript)) |
    ForEach-Object { $_.ToString("x2") } | Join-String
if ($actualHash -eq $expectedHash) {
    Invoke-Expression $setupScript
} else {
    throw "Script integrity check failed"
}

Common Errors and Fixes

  • User input in IEX creates injection vulnerability. Any script that passes user-supplied data — command-line arguments, web form data, file contents, or database values — into an Invoke-Expression call can execute arbitrary code. Replace with the & call operator or a script block that receives the input as a parameter.
  • Variable expansion happens before Invoke-Expression sees the string. If your string contains $variables, PowerShell expands them before Invoke-Expression runs. Use single quotes to delay expansion if you want Invoke-Expression to handle the expansion, but be aware this adds a layer of indirection that complicates debugging.

Related Cmdlets / See Also

Wrapping Up

For 95% of use cases that seem to require Invoke-Expression, the & call operator or a script block is safer, faster, and clearer. Reserve Invoke-Expression for genuine dynamic code generation from fully trusted sources, always validate with a cryptographic hash before executing downloaded scripts, and never pass user input into it under any circumstances.

Send-Item -To