title: Shell Command-Word Reassembly via Parameter Expansion
id: ATR-2026-02525
rule_version: 1
status: experimental
description: |
  Detects a shell command in which the WORD THAT WILL BE EXECUTED is not written literally but is produced at expansion time by a bash parameter transformation, indirection, or pattern substitution. The command a static safety classifier reads is therefore not the command the shell runs.
  Derived from CVE-2026-29783 (GitHub Copilot CLI <= 0.0.422), where the shell tool's safety assessment classified a command as "read-only" while bash parameter transformation operators reconstructed and executed a different command. The advisory names indirect prompt injection through repository files, MCP server responses, or user instructions as the entry point, so the reassembled command arrives as ordinary agent input.
  SHAPE, NOT VENDOR STRING. Nothing here is Copilot-specific. The invariant is positional: a legitimate command line names its executable literally ("git status", "$PYTHON -m pytest"). Computing the executable name inline at the call site -- ${!ref}, ${v//X/}, ${v^^}, ${v:3:4} in command position -- has no benign purpose; it exists to make the command word unreadable until after the classifier has run.
  DELIBERATE LIMITS, stated so the boundary is not mistaken for coverage:
  1. A plain expansion in command position ("$b -s http://host/x") is NOT
     matched and cannot be. It is lexically identical to "$PYTHON -m pip",
     which is everywhere in real scripts. When the reassembly happens in an
     earlier assignment and only a bare $var reaches the call site, this rule
     sees nothing. That residue is real and is not claimed.
  2. Suffix/prefix stripping (${f##*/}, ${f%.md}) is excluded from the
     operator set entirely. It is the single most common benign expansion in
     shell code and carries no execution semantics.
  3. Pattern substitution counts only when the replacement is EMPTY
     (${v//pat/}). Deleting characters is how a concealed command word is
     reassembled; ${v//pat/value} is ordinary templating and is excluded.
  4. @Q, @A and @K are not in the operator set. They serialise a value for
     printing or re-import rather than concealing a command word.

  FP LOG -- adversarial review 2026-08-23, run through the real engine on the canonical corpus shapes. Ten of sixteen near-boundary benign inputs fired against the first draft. What changed, and why each change is load-bearing:
  a. An @P-only condition was DELETED. It matched the bare token ${v@P}
     anywhere in any text, with no command context at all, so it fired on the
     bash manual's own wording for the operator, on a legitimate shell-tool
     description that mentions expanding ${cmd@P} before classification, and
     on real prompt-framework dotfile code (local rendered="${theme_line@P}").
     Excluding P-initial variable names did not help: theme_line, rendered and
     LP_PS1 are all real prompt variables that do not begin with P. No lexical
     narrowing separates a prompt preview from a payload, so the condition is
     gone. Cost, stated plainly: printf "${note@P}" -- an execution primitive
     hidden inside an output-only verb -- is no longer detected unless it
     reaches an evaluator (condition 2).
  b. Condition 1 now requires an ARGUMENT after the computed word. The opening
     anchor alone (line start, ;, &, |, "(" ) treated every markdown table row
     ("| ${var^^} | Uppercase |"), every parameter-expansion cheat sheet line,
     every parenthesised example in prose ("operators (${q//-/} and ${v^^})")
     and a Makefile recipe as "command position". Requiring a flag, URL or
     path to follow is what distinguishes an invocation from documentation.
  c. Both conditions dropped non-empty replacements, which removed
     eval "${SNIPPET//__PLACEHOLDER__/$prefix}" -- installer templating.
  d. A second probe round then broke the repaired condition 1 three more
     times, so its operator set was cut to indirection and deletion only.
     Substring extraction died because ${sha:0:7} followed by a path is the
     standard CI log and prompt idiom ("build | ${sha:0:7} /src/main.go"),
     where the pipe is a column separator and nothing is being executed at
     all. Case modification died because dispatch scripts really do write
     "${PM,,} -y install curl" and "${SERVER_MODE,,} --config /etc/app.yaml".
     Cost: an attacker who slices or case-folds the command word at the call
     site is not caught in command position. Both operators are still live in
     condition 2, where an explicit evaluator removes the benign reading.

  KNOWN RESIDUAL, not fixed and not hidden: a document that reproduces a working payload verbatim inside a fenced shell block still fires, because the payload is textually the attack. So does a script that genuinely computes a binary name inline and immediately passes it a flag (${TOOLCHAIN//-/} --version). Both are triage items, not silent passes.
author: ATR Community
date: 2026/08/23
schema_version: '0.1'
detection_tier: pattern
maturity: test
severity: high
references:
  cve:
    - CVE-2026-29783
  cwe:
    - CWE-78
  owasp_llm:
    - LLM01:2025
  owasp_agentic:
    - ASI02:2026
  mitre_atlas:
    - AML.T0051 - LLM Prompt Injection
    - AML.T0053 - LLM Plugin Compromise
  external:
    - https://github.com/github/copilot-cli/security/advisories
compliance:
  owasp_llm:
    - id: LLM01:2025
      context: The reassembled command reaches the shell tool as injected content from a repository file, MCP response, or instruction, which is the prompt-injection path the advisory names.
      strength: primary
  owasp_agentic:
    - id: ASI02:2026
      context: 'Tool misuse expressed as classifier evasion: the tool is invoked exactly as designed, but with an argument whose meaning changes after the authorization decision was made.'
      strength: primary
  eu_ai_act:
    - article: '15'
      context: Article 15 requires resilience against attempts to manipulate system behaviour. A command whose executable name only materialises after the safety check is precisely such a manipulation, and detecting it is the accuracy-and-robustness control the article asks for.
      strength: primary
    - article: '14'
      context: Human oversight is defeated when the string shown to the reviewer is not the string executed; this rule restores the reviewer's ability to see what will actually run.
      strength: secondary
    - article: '9'
      context: Obfuscated command execution belongs in the risk register for any agent with shell access; detection events are the ongoing monitoring evidence Article 9 requires.
      strength: secondary
  nist_ai_rmf:
    - function: Manage
      subcategory: MG.2.3
      context: Runtime treatment for the risk that an agent's own command-safety classification is bypassed by lexical obfuscation.
      strength: primary
    - function: Measure
      subcategory: MS.2.7
      context: Detection events document the security and resilience of the shell tool boundary, including attempts that the classifier alone would have passed.
      strength: secondary
    - function: Map
      subcategory: MP.5.1
      context: 'Catalogues classifier evasion as a distinct risk from direct command injection: the payload is well-formed shell, not malformed input.'
      strength: secondary
  iso_42001:
    - clause: '8.1'
      context: Operational control of the agent's shell execution path requires that the command evaluated by the guardrail is the command executed; this rule detects divergence.
      strength: primary
    - clause: '6.2'
      context: Preventing unreviewed command execution is an AIMS information-security objective, operationalised here as a runtime detection.
      strength: secondary
    - clause: '8.4'
      context: Impact assessment must account for shell commands that execute outside the reviewed set; these events supply that evidence.
      strength: secondary
tags:
  category: excessive-autonomy
  subcategory: command-safety-classifier-evasion
  scan_target: both
  confidence: medium
agent_source:
  type: tool_call
  framework:
    - any
  provider:
    - any
detection:
  condition: any
  conditions:
    - field: tool_args
      operator: regex
      value: (?:^|[\n;&|(])[ \t]*(?:["']\s*)?\$\{(?:![A-Za-z_][A-Za-z0-9_]{0,31}\}|[A-Za-z_][A-Za-z0-9_]{0,31}(?:@[PE]|/{1,2}[^}/]{0,24}/)\})["']?[ \t]+(?:-{1,2}[A-Za-z0-9]|https?://|/[A-Za-z0-9._~-]|\./|~/)
      description: Executable word in command position is produced by indirection, prompt/escape transformation or deletion-substitution rather than written literally, and is immediately handed a flag, URL or path argument
    - field: tool_args
      operator: regex
      value: (?i)\b(?:eval|\bbash\s+-c|\bsh\s+-c|\bzsh\s+-c|\bksh\s+-c)\s+(?:["']\s*)?\$\{(?:![A-Za-z_][A-Za-z0-9_]{0,31}\}|[A-Za-z_][A-Za-z0-9_]{0,31}(?:@[PE]|\^\^?|,,?|/{1,2}[^}/]{0,24}/|:[0-9]{1,4}:[0-9]{1,4})\})
      description: eval or an explicit shell -c is applied to a parameter expansion that rewrites its own value before execution
  false_positives:
    - A build or entrypoint script that genuinely computes a binary name inline and immediately passes it a flag, e.g. ${TOOLCHAIN//-/} --version. Rare but real; the rule fires and triage is required
    - Security documentation that reproduces a working payload verbatim inside a fenced shell block. The payload is textually the attack, so this cannot be excluded without excluding the attack
    - 'Prose, cheat sheets, markdown tables and dotfiles that merely NAME the expansion operators were measured and do NOT fire: the trailing argument requirement in condition 1 and the removal of the standalone @P condition are what keep them out'
response:
  actions:
    - alert
    - escalate
  auto_response_threshold: high
  message_template: |
    [ATR-2026-02525] Shell command whose executable word is computed by parameter expansion rather than written literally. The command a safety classifier reads is not the command that will run (CVE-2026-29783 shape). Expand the command manually before allowing it.
confidence: 68
