Skip to content

Add PowerShell Invoke-Expression parity with Bash command-string recursion #63

Description

@Aaronontheweb

Parity gap

The Bash parser recurses into static command strings for bash -c and sh -c.

The PowerShell parser recurses into pwsh -Command and pwsh -EncodedCommand. However, it does not recurse into Invoke-Expression or its iex alias.

These constructs have different process and scope rules. They still share one security property: each construct executes a string as shell code.

ShellSyntaxTree currently gives consumers different visibility into that code:

  • bash -c 'rm -rf /tmp/x' surfaces the inner rm clause.
  • iex 'Remove-Item C:\x' surfaces only the outer Invoke-Expression clause.

SPEC.POWERSHELL.md section 10 makes this difference explicit. PowerShell corpus case 157 locks the current behavior.

Why this matters

ShellSyntaxTree feeds security gates that evaluate the surfaced command verbs.

A consumer can inspect a static Bash command string. The same consumer cannot inspect an equivalent static PowerShell expression string.

A persistent approval for Invoke-Expression can then authorize a different payload later.

Netclaw PR #1733 exposes this difference in its PowerShell approval corpus:
netclaw-dev/netclaw#1733

Required behavior

ShellSyntaxTree should give both grammars the same safe-fail contract for command strings.

  • Recurse into a PowerShell expression string only when the parser proves that the string is static.
  • Apply the behavior to Invoke-Expression and the canonical iex alias.
  • Surface the inner clauses for consumer policy checks.
  • Preserve the current PowerShell location because Invoke-Expression uses the current scope.
  • Apply the existing input-size and recursion-depth limits.
  • Mark an uncertain payload as DynamicSkip or IsUnparseable.
  • Do not return a clean, persistently approvable Invoke-Expression clause for a dynamic payload.

Dynamic forms include variables, interpolation, concatenation, subexpressions, pipeline input, and other computed expressions.

Examples

Static input:

iex 'Remove-Item C:\x'

Expected result: the AST surfaces the canonical Remove-Item clause and its path.

Dynamic input:

Invoke-Expression $code

Expected result: the AST exposes the payload as dynamic.

Interpolated input:

iex "Remove-$noun C:\x"

Expected result: the AST exposes the payload as dynamic.

Acceptance criteria

  • Update SPEC.POWERSHELL.md section 10.
  • Replace corpus case 157 with static and dynamic cases.
  • Add coverage for Invoke-Expression and iex.
  • Add coverage for literal, interpolated, variable, and pipeline payloads.
  • Preserve PowerShell location attribution for static inner clauses.
  • Preserve the current public API unless the design requires an approved API change.
  • Keep all existing Bash and PowerShell corpus cases green.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions