← 返回博客列表

MCP STDIO 供应链安全:零点击 RCE 如何穿透你的 Agent IDE

过去一周,三份简报里反复出现同一个信号:MCP 的安全债务已经从「没鉴权」升级到「架构级供应链风险」。OX Security 把 STDIO 传输的命令注入称为 the mother of all AI supply chains;Codersera 的 2026 审计显示 43% 的 MCP Server 存在命令注入、四成不要求认证;Windsurf 的 CVE-2026-30615 已经实锤。与此同时,豆包和千问关停智能体入口、阿里禁用 Claude Code、Meta 承认 Agent 进展不及预期——大厂集体踩刹车,背后有一条共同逻辑:Agent 工具链的安全边界还没跟上功能扩张的速度

如果你在用 Cursor、Claude Code 或 Windsurf 跑本地 MCP Server,这篇文章讲的是最容易被忽视、但杀伤力最大的一类漏洞。

攻击面:为什么 STDIO 比想象中危险

很多人把 MCP 的 stdio 模式当成「天然安全」:Server 跑在本机子进程里,不暴露端口,似乎比 HTTP 远程部署省心。但 stdio 只解决了网络暴露问题,没解决进程执行问题。

典型本地 MCP 启动方式长这样:

{
  "mcpServers": {
    "my-tool": {
      "command": "npx",
      "args": ["-y", "some-mcp-package@latest"]
    }
  }
}

Client(IDE / Agent)会 fork 子进程,把 command + args 拼起来执行。问题出在两个环节:

  1. 安装阶段npx -y 会从 npm 拉最新包,README 里的 postinstall 脚本、package.json 的 bin 字段都可以执行任意命令
  2. 运行阶段:部分 Client 会把工具描述(tool schema / description)拼进 shell 命令或环境变量,攻击者可通过投毒的工具元数据注入

OX Security 披露的 Windsurf 案例(CVE-2026-30615)攻击链可以概括为:

恶意 MCP 包 README
    → 诱导用户 npx 安装
        → postinstall 写 ~/.config 启动项
            → 下次 IDE 启动 MCP 时零点击 RCE

更隐蔽的路径是工具描述投毒:Agent 在选工具时会把 description 字段喂给模型,如果 Server 返回的 description 里夹了 shell 元字符,而 Client 用不安全的方式拼接命令,就能在「用户完全没点运行」的情况下触发。

与远程 MCP 的安全对比

维度STDIO(本地)Streamable HTTP / SSE(远程)
网络暴露无端口监听需 TLS + 防火墙
鉴权常被忽略(「本地=可信」)OAuth 2.1 已成规范要求
命令执行Client 直接 fork shellServer 侧隔离,Client 只发 JSON-RPC
供应链npm/pip 包投毒风险极高容器镜像 + 签名验证
审计几乎无日志可接入 API Gateway 日志

远程 MCP 要过鉴权、限流、传输加密四道关(参见 MCP Server 生产实战),但本地 stdio 往往四道关全跳过。这就是审计里「四成 Server 不要求认证」的数字来源——不是远程部署忘了鉴权,而是大量 Server 压根跑在 stdio 模式下,压根没设计鉴权层。

Anthropic 对 OX Security 披露的回应是更新 SECURITY.md,把消毒责任甩给下游 Client 开发者。翻译成人话:协议层不会帮你挡命令注入,你得自己在 Client 侧做白名单

加固方案:Client 侧 command 白名单

不管你用 Cursor 还是自研 Agent,只要会 fork MCP Server 子进程,第一道防线是禁止任意 command 字符串

Python 示例:白名单 + 参数校验

import shlex
import subprocess
from dataclasses import dataclass

ALLOWED_COMMANDS: dict[str, list[str]] = {
    "npx": ["-y", "@modelcontextprotocol/server-filesystem"],
    "uvx": ["mcp-server-git"],
    "node": ["/opt/mcp/verified-server/index.js"],
}

@dataclass
class McpServerConfig:
    command: str
    args: list[str]

def validate_mcp_config(cfg: McpServerConfig) -> None:
    if cfg.command not in ALLOWED_COMMANDS:
        raise PermissionError(f"command not in allowlist: {cfg.command}")

    allowed_args = ALLOWED_COMMANDS[cfg.command]
    # 严格模式:args 必须完全匹配预注册配置
    if cfg.args != allowed_args:
        raise PermissionError(
            f"args mismatch for {cfg.command}: "
            f"got {cfg.args}, expected {allowed_args}"
        )

def spawn_mcp_server(cfg: McpServerConfig) -> subprocess.Popen:
    validate_mcp_config(cfg)
    # 不用 shell=True,避免元字符注入
    return subprocess.Popen(
        [cfg.command, *cfg.args],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        shell=False,
        env={"PATH": "/usr/local/bin:/usr/bin"},  # 最小化 PATH
    )

关键原则:

  • shell=False:永远不要把 command 字符串交给 /bin/sh -c
  • 固定 args:不允许用户/模型动态拼接 args,npx -y 后面的包名必须预注册
  • 最小 PATH:防止 PATH 劫持替换 npx / node
  • 版本锁定:用 npx @scope/pkg@1.2.3 而非 @latest

TypeScript 示例:Cursor 配置审计脚本

团队可以 CI 里跑一个配置审计,拒绝 @latest 和未知包名:

import { readFileSync } from "fs";

interface McpEntry {
  command: string;
  args?: string[];
}

const ALLOWED_PACKAGES = new Set([
  "@modelcontextprotocol/server-filesystem@1.0.0",
  "@modelcontextprotocol/server-github@0.6.0",
]);

function auditMcpConfig(configPath: string): string[] {
  const raw = JSON.parse(readFileSync(configPath, "utf-8"));
  const errors: string[] = [];

  for (const [name, entry] of Object.entries<McpEntry>(
    raw.mcpServers ?? {}
  )) {
    if (entry.command === "npx" && entry.args?.[0] === "-y") {
      const pkg = entry.args[1] ?? "";
      if (pkg.includes("@latest") || !pkg.includes("@")) {
        errors.push(`${name}: must pin exact version, got ${pkg}`);
      }
      if (!ALLOWED_PACKAGES.has(pkg)) {
        errors.push(`${name}: package not in allowlist: ${pkg}`);
      }
    }
  }
  return errors;
}

const errs = auditMcpConfig(".cursor/mcp.json");
if (errs.length) {
  console.error("MCP config audit failed:\n" + errs.join("\n"));
  process.exit(1);
}

工具描述投毒:第二道防线

即使 command 白名单做好了,Server 返回的 tool schema 仍可能投毒。防御策略:

  1. Schema 消毒:对 description 字段做长度限制 + 字符黑名单(`$;|$(
  2. 模型侧隔离:不要把原始 tool description 直接拼进 shell;tool 调用走结构化 JSON-RPC,不走字符串模板
  3. Mcpsnoop 抓包:Jul 4 简报提到的 Mcpsnoop 可以做 MCP 版 Wireshark,上线新 Server 前先抓一轮流量确认没有异常字段

arXiv 2601.17549 的实测数据也值得记住:MCP 的架构选择能把攻击成功率放大 23–41%,链式攻击对 Function Calling 和 MCP 都能打到 91–96%。单点加固不够,要假设工具链会被串联利用

和大厂「Agent 收缩」的关联

本周国内科技简报里,豆包和千问关停智能体入口、美团限制豆包、阿里禁用 Claude Code——表面是合规和产品策略,底层是同一类风险:Agent 能调外部工具后,攻击面从 prompt 扩展到整个本机执行环境

MateClaw 1.7.0 的更新方向很有代表性:审批不能挂死、长任务不能黑盒、Token 花销不能看不见。这三条恰好对应 Agent 生产化的「三道关」:

关卡对应风险MateClaw 1.7.0 做法
审批工具调用无人工确认审批流程防挂死
可观测黑盒执行无法审计长任务状态可视化
成本/安全Token 与权限失控Token 花销面板

MCP 供应链安全是第四道关:在工具接入层就做白名单,而不是等 RCE 了再补审批

行动建议

今天就做(15 分钟)

  1. 打开 .cursor/mcp.json(或 Claude Code / Windsurf 等价配置),列出所有 command + args
  2. npx -y xxx@latest 全部改成固定版本号
  3. 删除不再使用的 Server 条目——每多一个 Server 就多一个供应链入口

本周完成(团队级)

  1. 把上面的 TypeScript 审计脚本接进 CI,PR 改 MCP 配置必须过审
  2. 新接入 Server 前用 Mcpsnoop 抓包 + 在隔离 VM 里首次运行
  3. 制定团队 MCP 允许列表(package 名 + 版本 + 负责人)

延伸阅读

MCP 正在从「本地玩具」变成 Agent 工具链的主动脉。STDIO 模式的便利不该以零点击 RCE 为代价——白名单不是过度工程,是 2026 年跑 Agent IDE 的最低门槛