title: Shell Command Separator Inside a Path-Typed MCP Tool Argument
id: ATR-2026-02557
rule_version: 1
status: experimental
description: |
  Detects an MCP or agent tool invocation in which a structurally path-typed argument - projectPath, file_path, directory, filename, log_file_name, cwd, output_path - carries a shell command separator followed by a command that either takes an argument (a flag, or an absolute system path) or is a bare reconnaissance binary that ends the value.
  This is the shape reported as CVE-2026-25546 against godot-mcp, where executeOperation passed projectPath straight into exec(), so every tool that accepts a project path - create_scene, add_node, load_sprite and the rest - became a command-execution primitive. The same shape recurs across the MCP ecosystem wherever a typed path or identifier reaches a shell: MCP-for-Stata's log_file_name, and the broader family of servers that build command lines by string concatenation.
  WHAT ADVERSARIAL MEASUREMENT CHANGED (2026-08-23). The first draft of this rule asserted that a separator inside a path argument "has no legitimate meaning". Run against the real engine, that formulation fired on 11 of 18 realistic benign samples. Separators are legal POSIX filename characters and humans use them as PUNCTUATION: "Smith; John Resume.pdf" (the Lastname; Firstname convention), "Attention Is All You Need; Vaswani et al 2017.pdf", "Terms; Conditions v4 EN.docx", "Fleet Overview | Grafana" (a browser page title used as a save-as name), "Daft Punk | Alive 2007", "/opt/tools;/opt/sdk/bin" (a semicolon-delimited search path). Structurally "| id" and "| Grafana" are the SAME string shape, so a separator plus a word token cannot be the signal.
  What survives measurement is narrower and honest: the injected command must carry a shell TELL. Either it takes an argument that filename punctuation never takes - a flag terminated by whitespace, a quote or '=', or an absolute system path (/etc, /tmp, /var, /usr, /bin, ...) - or it is one of a deliberately tiny vocabulary of argumentless reconnaissance binaries (whoami, uname, hostname, ifconfig, netstat, lsblk, dmesg) that are not English filename words.
  This rule therefore IS partly a command-shape test, not the pure structural test the first draft claimed. It still adds detection the corpus does not have: verified 2026-08-23 against the full loaded corpus, "; ls -la /etc", "| whoami" and "; uname" through a path-typed argument produce a match from this rule and from no other. "; cat /etc/passwd", "; python3 -c" and "out.log; touch /tmp/pwn" are already covered by ATR-2026-00066 / 00111 / 01615, so this rule is additive there rather than load-bearing.
  CWE-78 (OS Command Injection), CWE-88 (Argument Injection), CWE-1287 (Improper Validation of Specified Type of Input).
author: ATR Community
date: 2026/08/23
schema_version: '0.1'
detection_tier: pattern
maturity: test
severity: high
references:
  cve:
    - CVE-2026-25546
  euvd:
    - EUVD-2026-25546
  owasp_llm:
    - LLM06:2025
  owasp_agentic:
    - ASI02:2026
    - ASI05:2026
  mitre_atlas:
    - AML.T0053 - LLM Plugin Compromise
  mitre_attack:
    - 'T1059.004 - Command and Scripting Interpreter: Unix Shell'
    - 'T1505.003 - Server Software Component: Web Shell'
  external:
    - https://github.com/Coding-Solo/godot-mcp/security/advisories
metadata_provenance:
  cve: human-reviewed
  owasp_llm: human-reviewed
  owasp_agentic: human-reviewed
  mitre_atlas: human-reviewed
  mitre_attack: human-reviewed
compliance:
  eu_ai_act:
    - article: '15'
      context: Article 15 requires accuracy, robustness and cybersecurity appropriate to the intended purpose. An MCP server that turns a path argument into a shell command line fails that requirement for every tool that accepts the argument; detecting the separator at call time is the compensating control while the server remains unpatched.
      strength: primary
    - article: '9'
      context: Article 9 risk management must record the tool surface an agent can reach. When one unvalidated path parameter converts an entire tool family into command execution, that concentration of risk belongs in the risk register and this rule supplies the runtime evidence for it.
      strength: secondary
  nist_ai_rmf:
    - function: Measure
      subcategory: MS.2.7
      context: MS.2.7 requires AI system security and resilience to be evaluated and documented. Inspecting typed tool arguments for shell syntax is a measurable, per-invocation control over the agent-to-tool boundary rather than an assumption that every MCP server validates its own input.
      strength: primary
    - function: Manage
      subcategory: MG.3.1
      context: MG.3.1 requires risks from third-party AI resources to be managed. MCP servers are third-party components whose input handling the deploying organisation cannot audit at scale, so enforcing argument typing at the client is the practical management control.
      strength: primary
    - function: Map
      subcategory: MP.5.1
      context: MP.5.1 requires adversarial techniques to be characterised. Argument injection through a structurally typed parameter is mapped here as distinct from command injection through a free-text command parameter, because the defensive assumption differs.
      strength: secondary
  iso_42001:
    - clause: '8.1'
      context: Clause 8.1 operational control over externally provided processes covers MCP servers invoked by the agent. This rule is the operational control applied to each invocation, independent of the server's own hardening.
      strength: primary
    - clause: '6.2'
      context: Clause 6.2 risk treatment requires proportionate controls. Because a single injected separator yields code execution at the server's privilege level, per-argument inspection is the proportionate treatment.
      strength: secondary
tags:
  category: tool-poisoning
  suppress_in_code_blocks: true
  subcategory: argument-injection-path-typed-parameter
  scan_target: tool_args
  confidence: high
  source: cve-disclosure
  vendor_sources: godot-mcp-cve-2026-25546
agent_source:
  type: mcp_exchange
  framework:
    - mcp
    - any
  provider:
    - any
detection:
  condition: any
  method: pattern
  false_positives:
    - 'MEASURED RESIDUAL: a document that quotes the attack payload verbatim inside a FOUR-SPACE INDENTED code block still matches. The engine''s code-block suppression (tags.suppress_in_code_blocks, enabled here) covers fenced blocks, inline backticks and markdown table cells, but not indented blocks - so a fenced or inline advisory is clean and an indented one is not. Verified 2026-08-23: the fenced advisory, the inline-backtick advisory, the CHANGELOG prose and this rule''s own YAML test cases are all clean; only the indented form fires.'
    - Security test suites, MCP server fixtures and CVE reproductions that pass a deliberately malicious path argument will match, which is the intended behaviour for a detection corpus.
    - A path-typed argument whose value legitimately contains a separator followed by a lowercase word AND a flag-shaped token - for example a filename fragment written as 'draft -final ' with a trailing space - would match. The flag must terminate at whitespace, a quote or '=', which is what keeps 'draft -final.pdf' and 'rollup -v2.csv' clean; the residual is the spaced form.
    - 'The engine compiles array-format regex conditions with flag ''i'' unconditionally (src/engine.ts compilePatterns) and IGNORES case_sensitive on this condition format. Every case-based discriminator is therefore unavailable to this rule. That is why the bare-recon vocabulary excludes ''id'', ''pwd'', ''groups'', ''arch'', ''env'' and ''ps'': each of them also matches an ordinary capitalised filename label such as ''/scans/Passport | ID'' or ''/design/Hero Banner | ps''.'
  conditions:
    - field: content
      operator: regex
      value: '"(?:[Pp]roject_?[Pp]ath|[Ff]ile_?[Pp]ath|[Dd]ir(?:ectory)?_?[Pp]ath|[Dd]irectory|[Ff]older|[Ff]ile_?[Nn]ame|[Oo]utput_?[Pp]ath|[Ii]nput_?[Pp]ath|[Ll]og_?[Ff]ile_?[Nn]ame|[Tt]arget_?[Pp]ath|[Ss]cene_?[Pp]ath|[Ss]ave_?[Pp]ath|cwd|[Ww]orkspace_?[Pp]ath)"\s*:\s*"[^"\n]{0,40}(?:;|&&|\|\||\s\|)\s{0,3}(?:\b[A-Za-z_][A-Za-z0-9_.-]{1,15}|/(?:usr/)?s?bin/[A-Za-z0-9_.-]{1,15})\s{1,2}(?:-{1,2}[A-Za-z][A-Za-z0-9-]{0,10}(?:\s|"|=)|/(?:etc|tmp|var|usr|bin|sbin|opt|root|home|dev|proc)[/\s"])'
      description: A path-typed tool argument is chained past a shell separator to a lowercase command that takes a flag or an absolute system path as its argument - argument injection into a server that concatenates the value into a shell string
    - field: content
      operator: regex
      value: '"(?:[Pp]roject_?[Pp]ath|[Ff]ile_?[Pp]ath|[Dd]ir(?:ectory)?_?[Pp]ath|[Dd]irectory|[Ff]older|[Ff]ile_?[Nn]ame|[Oo]utput_?[Pp]ath|[Ii]nput_?[Pp]ath|[Ll]og_?[Ff]ile_?[Nn]ame|[Tt]arget_?[Pp]ath|[Ss]cene_?[Pp]ath|[Ss]ave_?[Pp]ath|cwd|[Ww]orkspace_?[Pp]ath)"\s*:\s*"[^"\n]{0,40}(?:;|&&|\|\||\s\|)\s{0,3}\b(?:whoami|uname|hostname|ifconfig|netstat|lsblk|dmesg)\s{0,2}"'
      description: A path-typed tool argument is chained past a shell separator to a bare reconnaissance binary that terminates the value - the argumentless form of the same argument injection
response:
  actions:
    - alert
    - snapshot
    - escalate
  actions_rationale: 'response-action eligibility (docs/RESPONSE-ACTION-ELIGIBILITY.md): no FP measurement on the benign corpus; ceiling = observe. Removed block_tool(interrupt). Detection unchanged — recall cost is zero.'
  auto_response_threshold: high
  notify:
    - security_team
  message_template: |
    [ATR-2026-02557] HIGH: a path-typed tool argument contains a shell command separator followed by a second command. A filesystem path has no legitimate reason to contain ';', '&&', '||' or a spaced '|'. This is the CVE-2026-25546 (godot-mcp) pattern, where projectPath reached exec() and every tool that accepted a project path became a command-execution primitive. Block the call, and check whether the target server builds its command line by string concatenation - if so, every path-typed argument it exposes is affected, not just this one.
confidence: 77
