1. 从一个 mcp.json 文件说起:为什么配置文件能变成攻击入口
很多人第一次接触 MCP(Model Context Protocol)是在给编辑器或 Agent 工具接一个本地能力服务的时候。流程通常很简单:在项目根目录或者用户配置目录里放一个mcp.json,里面写清楚要启动哪个命令、传什么参数、设什么环境变量,然后重启客户端,工具列表里就多出几个能调用的函数。整个过程顺滑得让人放松警惕,因为大家潜意识里觉得"这不过是个配置文件"。
问题恰恰出在这个"不过是个配置文件"的认知上。mcp.json里有一个字段叫command,还有一个字段叫args。当客户端读取这份配置时,它会用command指定的可执行程序,拼接args里的参数,然后以子进程的方式把它拉起来。这个行为在 MCP 的 stdio 传输模式下是标准动作——客户端和服务端通过标准输入输出通信,服务端就是一个被拉起的本地进程。换句话说,一份 mcp.json 本质上等价于一条本机命令执行指令。你写什么,它就跑什么。
这就是标题里"一份 mcp.json 就是一次 RCE"的字面含义。RCE 是 Remote Code Execution(远程代码执行)的缩写,在安全语境里指的是攻击者能够在你机器上执行任意代码。而 MCP stdio 配置注入的核心逻辑是:只要攻击者能影响你mcp.json的内容,他就能让客户端替他执行命令。这个"影响"的途径比想象中多得多——可能是一个恶意仓库自带的配置文件,可能是一个被投毒的 MCP Server 安装脚本,也可能是一段看起来人畜无害的文档里让你"复制粘贴这段配置"。
我写这篇东西的出发点很直接:过去一段时间里,MCP 生态爆发式增长,各种 Server 层出不穷,从浏览器控制、数据库查询到设计稿读取,几乎每个工具都在教你"往 mcp.json 里加一段"。但很少有人系统性地讲清楚,这段配置背后到底发生了什么,以及哪些写法会把你自己送走。下面我会把五条真实可复现的攻击链拆开讲,再给一份能直接落地的加固清单。适合所有正在用 MCP、准备接 MCP Server、或者负责团队工具链安全的人看。不需要你是安全专家,但需要你愿意认真对待一个 JSON 文件。
2. MCP stdio 的工作机制:命令是怎么被拉起来的
2.1 stdio 传输模式的进程模型
要理解攻击链,先得把 stdio 模式的进程模型讲透。MCP 定义了多种传输方式,stdio 是最基础也最常用的一种。它的工作方式是这样的:客户端(比如某个 AI 编辑器、Agent 框架)读取配置,拿到command和args,通过操作系统的进程创建接口(在类 Unix 系统上是fork+exec系列,在 Windows 上是CreateProcess)启动一个子进程。这个子进程的标准输入和标准输出被客户端接管,双方用 JSON-RPC 格式的消息在管道里来回通信。
关键点在于,客户端并不校验command指向的程序是不是"合法的 MCP Server"。它只是忠实地执行配置里写的命令。你写npx some-mcp-server,它就去找 npx 然后跑;你写python evil.py,它就去跑 Python 脚本;你写bash -c "...",它就把那串东西交给 bash。客户端在这里扮演的是一个"无脑执行器"的角色,信任完全建立在配置文件本身的可信度上。
这个设计本身不算缺陷,因为配置文件理应由用户自己掌控。但现实是,配置文件的来源往往不受用户掌控。这就引出了后面所有的攻击面。
2.2 command、args、env 三个字段的真实语义
很多人对这三个字段的理解停留在表面,我逐个拆一下。
command是要执行的程序路径或名称。如果是不带路径的名称,系统会走PATH环境变量查找。这意味着攻击者可以利用 PATH 劫持——如果他能往 PATH 里靠前的目录放一个同名程序,你的配置就会跑到他的程序上去。这个手法在本地提权和供应链攻击里非常经典。
args是参数数组。注意它是数组,不是字符串,所以正常情况下每个元素会被当作独立参数传递,不会经过 shell 解析。这一点很重要:如果客户端实现规范,args里的内容不会被 shell 解释,那么; rm -rf /这种注入是无效的。但问题在于,很多客户端的实现并不规范,或者某些 Server 的启动方式本身就要求经过 shell。一旦经过 shell,args里的元字符就会生效,注入面瞬间打开。
env是环境变量。这个字段经常被忽视,但它同样危险。环境变量可以影响动态链接器的行为(比如LD_PRELOAD、DYLD_INSERT_LIBRARIES),可以覆盖程序查找路径(PATH、PYTHONPATH、NODE_OPTIONS),还可以传递各种凭据。一个被污染的env字段,不需要改command就能实现代码执行。
2.3 客户端信任边界在哪里断裂
把上面几点串起来,信任边界的问题就清楚了。理想情况下,信任边界应该画在"用户亲手写的配置"和"外部输入"之间。但实际使用中,这条边界被反复跨越:
- 你从 GitHub clone 了一个项目,项目里带了
.mcp.json,你的编辑器自动读取了它。 - 你按照某个教程复制了一段配置,没仔细看
command后面跟的是什么。 - 你安装了一个 MCP Server 包,它的安装脚本顺手改了你的全局配置。
- 你的配置文件放在一个被云同步、被其他工具读写的目录里。
每一条都是信任边界断裂的实例。而断裂之后,从"配置被污染"到"命令被执行"之间,几乎没有缓冲。
3. 五条真实攻击链的完整拆解
这一节是全文的核心。我把五条攻击链按"利用难度从低到高、隐蔽性从弱到强"排列,每条都给出触发条件、执行路径和可观测的痕迹。需要说明的是,这些链条的描述目的是防御,所有细节都停留在原理层面,不提供可直接武器化的完整载荷。
3.1 攻击链一:仓库自带配置文件的自动加载
这是门槛最低的一条。很多 AI 编辑器和 Agent 工具支持"项目级配置",也就是在项目根目录放一个.mcp.json或类似名字的文件,打开项目时自动加载。攻击者只需要在一个看起来正常的开源项目里塞进这样一份配置,内容大致是让command指向一个项目内的脚本,args传几个参数。
受害者 clone 项目、用编辑器打开,配置被自动读取,脚本被执行。整个过程没有任何弹窗确认。脚本可以做得非常克制——先只做一件事:把当前环境信息回传,或者写一个标记文件。等你发现的时候,可能已经过去很久了。
这条链的关键在于自动加载这个行为。手动加载的配置至少给了用户一次"我要不要加"的决策机会,自动加载把这个机会抹掉了。我个人的习惯是,任何会自动读取项目内配置的工具,我都会先去设置里把自动加载关掉,改成手动确认。
3.2 攻击链二:npx 与 uvx 的包名混淆
第二条链利用的是包管理器的动态解析特性。很多 MCP Server 的推荐配置长这样:command是npx,args是["-y", "some-mcp-server"]。这里的some-mcp-server是一个包名,npx 会去 npm registry 上找这个包并执行。
攻击面在于包名。如果这个包名没有被原作者注册,或者存在拼写相近的包(比如把figma-mcp写成figma-mcp-server、figma_mcp),攻击者可以抢注一个同名或近似名的包,里面放上恶意代码。用户复制配置的时候不会去核对包的真实来源,npx 一跑,恶意包就执行了。
uvx(Python 生态的类似工具)面临同样的问题。更麻烦的是,npx -y和uvx默认会跳过确认直接安装执行,连"这个包你没装过,确定要装吗"的提示都没有。我见过太多配置里直接写npx -y xxx,这个-y就是省掉确认的开关,方便是方便,风险也是实打实的。
防御这条链的办法很朴素:配置里尽量用完整路径或已锁定的本地安装,不要依赖运行时的动态包解析。如果非要用 npx,至少把包名核对到官方仓库的确切地址,并且考虑用--package指定版本。
3.3 攻击链三:env 字段里的动态链接器注入
第三条链更隐蔽,因为它不改command,只改env。前面提过,环境变量能影响动态链接器的行为。在 Linux 上,LD_PRELOAD可以让程序在启动时优先加载指定的共享库;在 macOS 上,对应的机制是DYLD_INSERT_LIBRARIES。攻击者只要在env里加上这么一条,指向一个他控制的.so或.dylib文件,那么被拉起的 MCP Server 进程在启动瞬间就会执行他的代码。
这条链的可怕之处在于,command看起来完全正常——就是一个普通的 MCP Server 启动命令,用户扫一眼根本不会起疑。危险藏在env里那一行不起眼的变量。而且这种注入发生在进程初始化的极早期,很多应用层的日志和监控根本来不及记录。
防御上,我建议对env字段采取白名单策略。只允许传递 Server 明确需要的变量,比如 API Key、数据库连接串这类业务凭据,绝不允许出现LD_、DYLD_、PYTHONPATH、NODE_OPTIONS这类能影响运行时行为的变量。如果某个 Server 的文档要求你设置这些,那本身就是一个危险信号,值得重新评估要不要用它。
3.4 攻击链四:shell 包装导致的参数注入
第四条链针对的是那些"必须经过 shell 才能启动"的场景。有些 MCP Server 的启动命令比较复杂,需要管道、重定向或者条件判断,配置里就会写成command: "bash"、args: ["-c", "some command with $VARIABLE"]这种形式。一旦经过 shell,args里的内容就会被 shell 解析,注入面彻底打开。
具体来说,如果args里拼接了任何来自外部的、未经过滤的字符串——比如从环境变量读来的路径、从配置文件读来的参数——攻击者就可以通过控制这个字符串来注入 shell 元字符。一个分号、一个反引号、一个$(),就能在原本的命令后面追加一条完全不同的命令。
这条链的触发条件比前几条苛刻一些,需要配置里存在"外部可控字符串进入 shell 命令"的模式。但在实际项目里,这种模式并不罕见,尤其是那些为了"灵活"而把路径、参数做成可配置的 Server。防御原则很简单:能不用 shell 就不用 shell。如果非用不可,所有进入 shell 的变量都必须经过严格转义,或者改用参数数组的形式传递。
3.5 攻击链五:配置同步与共享带来的横向扩散
最后一条链不针对单台机器,而是针对"配置的传播"。很多人会把mcp.json放在云盘同步目录、团队共享仓库、或者 dotfiles 管理工具里。这本身是好习惯,方便多设备一致。但一旦这份配置被污染,污染就会随着同步扩散到所有设备。
更隐蔽的是团队场景。一个团队共用一份 MCP 配置模板,某天有人在模板里加了一个"方便大家"的 Server,其他人拉取更新后自动生效。如果这个 Server 是恶意的,或者它的启动命令被篡改过,整个团队在同一时间被拿下。这种横向扩散的杀伤力远大于单点攻击。
防御这条链需要从流程入手:配置文件纳入版本控制时要有 review 机制,任何对command、args、env的改动都要有人工确认;云同步目录里的配置文件要定期审计;团队模板要有明确的维护者和变更记录。
4. 从配置到执行:攻击者视角的完整链路复盘
把五条链放在一起看,会发现它们共享一个三段式结构:污染入口 → 解析放大 → 执行落地。理解这个结构,比记住五条链本身更重要,因为新的攻击手法大概率还是这个骨架的变体。
污染入口是攻击者接触配置文件的途径。可能是仓库、可能是包名、可能是文档、可能是同步渠道。这一段的防御核心是"控制配置来源",只从可信渠道获取配置,对任何自动加载保持警惕。
解析放大是配置文件被客户端解读的过程。不同的客户端实现差异很大:有的严格按数组传参,有的会经过 shell,有的会自动展开变量,有的会合并多份配置。这些差异决定了同样的配置在不同客户端上的实际行为可能完全不同。这一段的防御核心是"了解你用的客户端到底怎么解析配置",不要假设它和别的客户端一样。
执行落地是命令真正跑起来的那一刻。到了这一步,防御手段已经很有限了,主要靠运行时的沙箱、权限隔离和监控。所以真正有效的防御必须前移,在污染入口和解析放大阶段就把问题拦住。
我自己的做法是给 MCP Server 单独建一个低权限的系统账户,所有 Server 都以这个账户运行,能访问的文件和网络都受限。这样即使某条链被触发,攻击者拿到的也只是一个受限环境,而不是我的整个工作账户。这个隔离成本不高,但收益很大。
5. 加固清单:从配置写法到运行时隔离
下面这份清单是我在实际使用中逐步积累的,按"配置层 → 客户端层 → 运行时层"三层组织。每一条都对应前面讲过的具体风险,不是泛泛而谈。
5.1 配置层的写法规范
| 风险点 | 危险写法 | 推荐写法 |
|---|---|---|
| 命令来源 | npx -y some-package | 本地已安装的完整路径,或锁定版本的调用 |
| 参数传递 | bash -c "cmd $VAR" | 参数数组,避免 shell 解析 |
| 环境变量 | 传入LD_PRELOAD、NODE_OPTIONS | 白名单,只传业务必需的凭据 |
| 路径引用 | 相对路径、依赖 PATH 查找 | 绝对路径,明确指向可信位置 |
| 配置来源 | 自动加载项目内配置 | 手动确认,或只加载用户级配置 |
配置层的第一原则是显式优于隐式。所有能让客户端"自己猜"的地方,都改成明确写死。包名写全、路径写绝对、版本写死、变量列全。多打几个字,换来的确定性是值得的。
第二原则是最小权限。配置里只放这个 Server 跑起来必需的东西,多余的变量、多余的参数一律不加。很多注入链之所以能成立,就是因为配置里存在"本来不需要但顺手加上了"的字段。
5.2 客户端层的加载策略
客户端的选择和配置同样重要。我在选工具时会重点看几个点:它是否支持项目级配置的自动加载?自动加载能不能关?加载配置时有没有确认提示?对command和env有没有基本的校验?
如果某个工具默认自动加载项目内配置且无法关闭,我会非常谨慎地使用它,或者干脆把它跑在隔离环境里。如果它加载配置时连个提示都没有,那这个工具的安全模型基本等于没有。
还有一个容易被忽视的点:配置的合并顺序。有些客户端会同时读取用户级配置和项目级配置,然后合并。如果合并逻辑是"项目级覆盖用户级",那么一个恶意项目就能覆盖你精心设置的安全配置。了解你所用客户端的合并规则,必要时把项目级配置的加载关掉。
5.3 运行时层的隔离与监控
到了运行时层,目标不再是"阻止执行",而是"限制执行的影响范围"和"留下可追溯的痕迹"。
隔离方面,我推荐至少做到三点:用独立的低权限账户运行 MCP Server;用容器或沙箱限制文件系统和网络访问;对敏感目录设置明确的拒绝规则。这三点不需要多复杂的工具,系统自带的账户管理和权限控制就能做到大部分。
监控方面,重点是记录"哪个配置启动了哪个进程"。很多系统层面的进程审计工具可以做到这一点,把 MCP Server 的启动事件单独标记出来,定期 review。一旦发现某个 Server 的启动命令和配置里写的不一致,或者启动了配置里没写的子进程,就是一个明确的告警信号。
提示:不要等到出事才去翻配置。养成定期审计
mcp.json的习惯,尤其是command、args、env三个字段。任何一次改动都值得你花两分钟确认。
6. 几个我踩过的坑和常见误区
讲完方法论,说几个具体的坑。这些都是我自己或者身边人真实遇到过的,写出来希望能帮你省点事。
第一个坑是以为 JSON 格式合法就安全。JSON 只是语法,语法正确不代表内容安全。一份格式完美、缩进整齐的mcp.json,完全可以是恶意的。不要用"看起来很正常"来判断配置的安全性,要看字段的实际语义。
第二个坑是忽视env字段。我早期审配置只看command和args,觉得env就是传个 API Key 而已。直到有一次看到一份配置在env里塞了NODE_OPTIONS=--require ./hook.js,才意识到这个字段的威力。现在我看配置,env是第一个看的。
第三个坑是盲目复制教程里的配置。网上很多 MCP 教程为了让你快速跑通,直接给一段配置让你粘贴。这些配置里的包名、路径、参数往往没有经过安全审视。复制之前,至少把command指向的程序搞清楚是什么,args里的每个参数是什么意思。
第四个坑是把 MCP Server 当成普通依赖。普通依赖出问题,影响的是构建产物;MCP Server 出问题,影响的是你的运行环境,因为它是在你机器上以你的权限跑起来的进程。这个区别决定了 MCP Server 需要比普通依赖更严格的准入。
第五个坑是以为隔离了网络就安全。有些攻击链根本不需要网络,本地文件读写、本地命令执行就够了。网络隔离能挡住数据外传,但挡不住本地破坏。隔离要全面,不能只做一半。
7. 关于 MCP 生态安全的一点个人判断
MCP 这个协议本身设计得不算差,stdio 模式的选择也有它的合理性——简单、通用、跨平台。问题不在协议,而在生态的成熟度。现在的 MCP 生态有点像早期的 npm,包数量爆发式增长,但准入、审计、签名这些基础设施还没跟上。在这个阶段,安全责任很大程度上落在使用者自己身上。
我的判断是,接下来一段时间里,MCP 配置注入类的问题会越来越多,因为攻击成本低、收益明确、防御意识普遍不足。等到生态成熟、工具链内置了足够的防护,这类问题才会慢慢减少。在那之前,把mcp.json当成一份"会被执行的代码"来对待,是最实际的自我保护。
具体到操作上,我给自己定了几条硬规矩:任何新加的 MCP Server,先在一个隔离环境里跑一遍,观察它启动了什么进程、访问了什么文件、连了什么地址;确认没问题再进主环境。任何对现有配置的改动,都走一次人工 review,哪怕只是改个参数。任何来自外部的配置片段,先拆开看字段,再决定要不要用。
这些规矩听起来麻烦,但比起某天发现自己的机器被一份 JSON 文件拿下的代价,这点麻烦实在不算什么。配置文件从来都不只是配置,它是你交给工具的一份信任契约。契约的条款,值得你逐字读一遍。