title: Command Injection in create-mcp-server-stdio via Unsafe exec() Concatenation (CVE-2025-54994)
id: ATR-2026-00577
rule_version: 2
status: experimental
description: |
  GitHub Security Advisory GHSA-3ch2-jxxc-v4xf (CVE-2025-54994). The create-mcp-server-stdio npm package builds shell commands by concatenating MCP stdio tool parameters directly into exec(), so shell metacharacters supplied through a tool argument (; | && $() backticks) are interpreted by the shell and execute arbitrary commands on the server host (RCE).
author: ATR Community (vulnerablemcp sync)
date: 2026/06/12
schema_version: '0.1'
detection_tier: pattern
maturity: experimental
severity: critical
references:
  owasp_llm:
    - LLM06:2025
    - LLM05:2025
  owasp_agentic:
    - ASI02:2026
    - ASI05:2026
  mitre_atlas:
    - AML.T0053 - AI Agent Tool Invocation
    - AML.T0051.001 - Indirect
  cve:
    - CVE-2025-54994
  cwe:
    - CWE-78
  ghsa:
    - GHSA-3ch2-jxxc-v4xf
  vulnerablemcp_id:
    - cve-2025-54994-command-injection-mcp-stdio
  external:
    - https://github.com/advisories/GHSA-3ch2-jxxc-v4xf
metadata_provenance:
  vulnerablemcp: vulnerablemcp-sync
  cve: vulnerablemcp-sync
  cwe: vulnerablemcp-sync
compliance:
  owasp_agentic:
    - id: ASI02:2026
      context: OWASP Agentic ASI02:2026 is exercised by command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994); this rule provides runtime detection of that technique.
      strength: primary
    - id: ASI05:2026
      context: OWASP Agentic ASI05:2026 is exercised by command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994); this rule provides runtime detection of that technique.
      strength: secondary
  owasp_llm:
    - id: LLM06:2025
      context: OWASP LLM LLM06:2025 is exercised by command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994); this rule is a detection implementation for that category.
      strength: primary
    - id: LLM05:2025
      context: OWASP LLM LLM05:2025 is exercised by command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994); this rule is a detection implementation for that category.
      strength: secondary
  eu_ai_act:
    - article: '15'
      context: EU AI Act Article 15 (accuracy, robustness and cybersecurity) requires controls against command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994); this rule provides runtime detection evidence for that obligation.
      strength: primary
    - article: '9'
      context: EU AI Act Article 9 (risk management system) requires controls against command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994); this rule provides runtime detection evidence for that obligation.
      strength: secondary
  nist_ai_rmf:
    - function: Manage
      subcategory: MG.2.3
      context: NIST AI RMF MG.2.3 (risk treatment options selected and tracked) is supported by this rule's detection of command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994).
      strength: primary
    - function: Measure
      subcategory: MS.2.7
      context: NIST AI RMF MS.2.7 (security and resilience evaluated and documented) is supported by this rule's detection of command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994).
      strength: secondary
  iso_42001:
    - clause: '8.1'
      context: ISO/IEC 42001 Clause 8.1 (operational planning and control, including control of externally-provided processes) is operationalised by this rule's detection of command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994).
      strength: primary
    - clause: '8.3'
      context: ISO/IEC 42001 Clause 8.3 (AI risk treatment) is operationalised by this rule's detection of command injection via unsafe exec() concatenation in create-mcp-server-stdio (CVE-2025-54994).
      strength: secondary
tags:
  category: tool-poisoning
  scan_target: runtime
  confidence: high
agent_source:
  type: mcp_exchange
  framework:
    - any
  provider:
    - any
detection:
  condition: any
  false_positives: []
  conditions:
    - field: content
      operator: regex
      value: (?i)(?:create-mcp-server-stdio[^\n]{0,200}\bexec\s*\(\s*(?:[`"'\x27][^`"'\x27\n]{0,160}[`"'\x27]\s*\+|`[^`\n]{0,160}\$\{)|\bexec\s*\(\s*(?:[`"'\x27][^`"'\x27\n]{0,160}[`"'\x27]\s*\+|`[^`\n]{0,160}\$\{)[^\n]{0,200}create-mcp-server-stdio)
      description: Detects the create-mcp-server-stdio exec() construct that concatenates a variable into the shell command string (string + concatenation or template literal ${...} interpolation), the exact CVE-2025-54994 sink.
    - field: tool_input
      operator: regex
      value: (?i)create-mcp-server-stdio[^\n]{0,200}\bexec\s*\([^)\n]{0,160}(?:;\s*\S|&&\s*\S|\|\|\s*\S|\|\s*\w|`[a-z][\w/.-]*`|\$\([a-z])
      description: Detects an MCP stdio tool parameter routed into create-mcp-server-stdio's exec() that already carries shell metacharacters (command separators, pipes, backtick or $() command substitution), indicating live injection.
    - field: content
      operator: regex
      value: (?i)create-mcp-server-stdio[^\n]{0,160}(?:\bexec\s*\([^)\n]{0,120})(?:\brm\s+-rf|\bnc\s+-[a-z]*e|/bin/sh|\bbash\s+-i|\bcurl\s+[^\n|]{0,80}\|\s*(?:sh|bash)|\bwget\s+[^\n|]{0,80}\|\s*(?:sh|bash))
      description: Detects create-mcp-server-stdio exec() carrying a classic post-injection payload (reverse shell, destructive rm, or curl|sh dropper) inside the concatenated command.
    - field: content
      operator: regex
      value: (?i)(?:create-mcp-server-stdio[^\n]{0,200}\bexec\s*\([^)\n]{0,160}(?:`[a-z][^`\n]{0,80}`|\$\(\s*[a-z])|\bexec\s*\([^)\n]{0,160}(?:`[a-z][^`\n]{0,80}`|\$\(\s*[a-z])[^\n]{0,200}create-mcp-server-stdio)
      description: Detects SHELL COMMAND SUBSTITUTION (backtick `cmd` or $(cmd)) inside a create-mcp-server-stdio exec() call on the content channel. The other content conditions only fire on the source-code sink shape (string + concatenation or template ${...}) or on a classic post-injection payload, so a substitution payload that carries neither - e.g. exec("echo `whoami`") - was uncovered. Command substitution is required to be co-framed with the create-mcp-server-stdio marker (within 200 chars on either side of the exec sink), so ordinary shell backticks elsewhere in a document cannot fire this critical block_tool rule. Bare command separators (; && || |) are DELIBERATELY excluded here because they are common in legitimate static commands (exec("npm run build && npm test")); those forms stay covered by the tool_input condition and by the payload condition.
response:
  actions:
    - alert
    - block_tool
  message_template: '[ATR-2026-00577] CVE-2025-54994: create-mcp-server-stdio passes MCP stdio tool input into exec() with shell metacharacters, enabling arbitrary command execution (CWE-78 / RCE). Remediation: upgrade create-mcp-server-stdio to a patched release, and replace exec() with execFile()/spawn() using an argument array (never concatenate tool input into a shell string); validate and sandbox all MCP stdio server input.'
  notify:
    - security_team
confidence: 85
_llm_authored:
  model: claude (gstack subagent)
  generalization_note: |-
    The rule generalizes beyond the literal PoC by anchoring on create-mcp-server-stdio together with its specific exec() concatenation sink: (1) string + concatenation of a variable into the shell command, (2) template literal ${...} interpolation, and (3) live shell metacharacters (; | && $() backticks) or classic post-injection payloads (reverse shell, rm -rf, curl|sh) appearing inside that exec() call. The create-mcp-server-stdio marker may appear within 200 chars on EITHER side of the exec() sink (e.g. as a leading handler label or a trailing handler comment), so the package context is still required but its position relative to the sink is not fixed. It deliberately does NOT match the JSON "command"/"args" config arrays (Flowise/LibreChat/litellm/mcp-stdio-config rules) nor the --mcp CLI flag form (PraisonAI), so the surface is unique to the create-mcp-server-stdio exec() concatenation construct. Safe execFile/spawn with argument arrays, scaffold CLI usage, library imports, and advisory/research text are excluded.
    rule_version 2 (content-channel command substitution): the original three conditions left a hole. The only condition covering backtick / $() command substitution declared field: tool_input, and no production event builder fills that field (src/hook-handler.ts emits tool_name / tool_args / content / tool_response; only the measurement harnesses in scripts/lib/corpus-event.ts fill tool_input), so on the runtime path a substitution payload with neither a concatenation sink nor a classic post-injection payload -- exec("echo `whoami`") -- was undetected. A fourth condition mirrors the substitution shape onto the content channel, bidirectionally anchored on the create-mcp-server-stdio marker exactly like condition 1. Deliberate scope limit: bare command separators (; && || |) are NOT added to the content channel, because they appear in legitimate static commands (exec("npm run build && npm test")) and this rule is severity critical with block_tool; those forms remain covered by the tool_input condition and, when they carry a real payload, by the post-injection condition. Known residual FP shape, shared with conditions 1 and 3: hardening documentation that quotes the vulnerable construct verbatim next to the package name will match.
  note: Generation-time authoring; verified by deterministic gate. Runtime detection is pure regex. Human review required before merge.
