Skip to content

powershell-format: $loaders enumeration leaves an edge — Invoke-Command/Start-Job -FilePath with a computed path #1490

Description

@kyle-sexton

The residual

plugins/powershell-format/hooks/powershell-format.sh decides whether a PSScriptAnalyzer custom rule's code-loading targets can be pinned to a trust approval. Named loaders are matched against an enumerated list:

$loaders = @("Import-Module", "ipmo", "Add-Type", "New-Module",
    "Import-PowerShellDataFile")

Any cmdlet that loads code but is not on that list is not treated as a loader, so a non-constant argument to it is not refused. Two named examples:

  • Invoke-Command -FilePath $x
  • Start-Job -FilePath $x

Both execute a repository file whose path the scan cannot pin, and neither trips the loader test.

Why generalizing is not the answer

The obvious generalization — "refuse any command with a non-constant argument" — would refuse Write-Output $x, Where-Object { $_ -eq $y }, and essentially every real rule module. That disables the feature for every consuming repo, which is a worse outcome than the residual: a gate nobody can satisfy gets switched off, and then nothing is checked at all.

There is no obvious middle predicate either. "Loads code" is not a property the AST exposes; it is cmdlet semantics, which means any mechanical test is either an enumeration (this one, with an edge) or over-broad (unusable). Enumerating more names narrows the edge without removing it.

Why the reported repro is nonetheless covered

The finding that surfaced this class was . (Join-Path $PSScriptRoot "deps" "helper.ps1"), and that is structurally covered without reference to $loaders:

  • InvocationOperator identifies every . and & invocation regardless of name
  • a target that is not a StringConstantExpressionAst, an expandable string whose variables the scan can expand, or an inline script block refuses approval

So the dot-source and call-operator forms — the ones a rule module actually uses to pull in a sibling file — do not depend on the list at all. The list only governs named cmdlets.

Fail-closed confirmation

The residual fails closed in the common direction and open only in the narrow one:

  • an unlisted cmdlet with a constant argument: the path is collected by the literal scan and pinned, so a change revokes the approval — correct
  • an unlisted cmdlet with a non-constant argument: not refused, and the target is not pinned — the bypass

The exposure is therefore limited to a rule module that both uses one of these specific cmdlets and computes its path. That is narrow, but it is real, and it is recorded here rather than left implicit.

What would actually close it

Options, none free:

  1. Extend the enumeration and accept a permanently-thin edge. Cheapest; does not close the class.
  2. Enumerate on the parameter rather than the cmdlet — any command invoked with -FilePath, -LiteralPath, -Path, -ScriptBlock and a non-constant value. Broader coverage, still an enumeration, but of a much smaller and more stable vocabulary than the cmdlet namespace.
  3. Invert: allow-list the cmdlets a custom rule may call at all, refuse everything else. Genuinely closes it, at the cost of refusing every rule module that calls anything unanticipated — likely unusable.

Option 2 looks like the best ratio if this is ever worth revisiting.

Provenance

Raised as a review finding on #1097, assessed there as a residual the design deliberately does not cover, and accepted as such on the record. Filed so the boundary is documented rather than rediscovered.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: lowNice-to-have, cosmetic, or speculative; opportunistic.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions