mcp.json安全体检:14项配置风险与npx供应链防护
2026/9/11 13:24:26 网站建设 项目流程

群里的消息我到现在还记得:一个同事甩了条命令,说装上之后 AI 能直接读数据库、搜代码、操作浏览器。不到五分钟,办公室至少六个人复制粘贴运行了。新加入的 MCP 生态项目越来越多,教程里超过一半的安装步骤都是这种模式——一条npx ... server命令,自动生成配置文件,然后重启应用。问题是,几乎没有人打开过那个被自动创建出来的mcp.json,看看里面到底写进去了什么。

这篇文章想做的,就是把mcp.json这个文件拉出来单独审一遍。它已经从"一个普通配置文件"逐渐变成客户端启动时的执行入口,攻击面比大多数人想象的要大得多。我会先从为什么它危险讲起,再逐项拆解本地体检时最值得关注的 14 项配置风险,最后给出一条 npx 命令就能在本地完成检查的落地思路,以及检查结果出来后怎么修、怎么判断误报。

1. mcp.json 为什么突然变成了高危文件

1.1 从"一个配置文件"到"执行入口"

传统插件系统里,配置文件一般只是声明"我有哪些功能",真正运行逻辑在已安装的插件包里。MCP(Model Context Protocol)不一样,mcp.json里直接记录了如何启动一个外部程序:用什么命令、传什么参数、带哪些环境变量。这意味着这个文件本质上是一个"启动脚本清单",只要客户端一加载,里面声明的进程就会被拉起。

如果你把 MCP 客户端理解成浏览器,那 mcp.json 的角色相当于"启动时要访问的 URL 列表 + 要执行的本地命令列表",而且很多场景下没有任何二次确认。程序员看浏览器尚且知道不能乱点链接,但看 mcp.json 时却常常有一种"配置嘛,能有什么坏心思"的错觉。

1.2 mcp.json 里到底存了什么

现在主流的 MCP 客户端,比如 Cursor、Claude Desktop、VS Code Copilot 等,配置格式大同小异。以 Cursor 的~/.cursor/mcp.json为例:

{ "mcpServers": { "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"] }, "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "ghp_xxxxxxxx" } } } }

这段 JSON 看起来人畜无害,但它表达的意图是:每次客户端启动,用npx去下载并执行名为@modelcontextprotocol/server-filesystem的包,同时给github这个 server 注入一个真实的 GitHub Token。

这里有几个关键点:

  • command字段指定可执行程序,恶意者可让它指向/tmp/evil.sh
  • args字段可以携带-y--allow-write这类自动确认或提权参数;
  • env字段可以注入LD_PRELOADNODE_OPTIONS等会影响进程行为的环境变量;
  • url字段可以指向任意远程服务,未经校验就可能成为数据外传通道。

1.3 谁在读取这份文件

风险进一步放大的原因在于读取方不只有一个。IDE 会读,命令行工具会读,后台 daemon 也会读。项目级的.mcp.json还可能被团队仓库直接提交,每个 clone 项目的人都会在本地加载同一份配置。

也就是说,这份文件一旦被投毒,影响范围是"所有信任该配置的客户端进程"。它不是数据文件,而是带着执行语义的指令文件。一个团队里只要有人被诱导提交了一个恶意的项目级配置,整个团队的本机环境都会跟着遭殃。这也是我把它称为"攻击面"的根本原因——你不可能在每个客户端都做二次弹窗确认。

2. 一条 npx 命令背后的供应链链路

2.1 npx 的便利与盲区

npx的定位是"免安装执行 npm 包",它解决了全局安装污染和版本切换的痛点。但这个机制有个天然盲区:它默认会从 npm registry 拉取并执行代码,而且很多教程为了让用户无障碍复现,会直接加上-y参数跳过确认。于是用户在很多情况下根本没看过要执行的包名,也没有锁定版本。

假设有人发布一个名为@modelcontextprotocol/server-filsystem的包,正版是filesystem少了一个字母e,肉眼几乎看不出差别。如果开发者在搜索或拼写时漏了一个字母,npx 会老老实实地把拼错的那个包拉下来执行。这种攻击叫 typosquatting,在 npm、PyPI 生态里已经见到过大量案例。

2.2 包名相似、依赖混淆、postinstall 脚本

npm 包除了自身代码,还可以带postinstall脚本,在安装完成后自动执行。也就是说,即使某个包的主代码是安全的,只要它的安装脚本有问题,你运行 npx 的瞬间就已经执行了那段代码。依赖混淆则是另一种常见手法:npm 仓库里同名同版本但内容被替换的包,或者公司内部包被上传到公共 registry,解析时可能拉取到错误来源。

npx skills add git@github.com:someone/xxx.git这类命令在技术圈越来越常见,大家为了省事,直接把 URL 丢给 npx 去拉。npx 对 GitHub 仓库的依赖分支不会做太多校验,分支名、commit hash 是否锁定,完全看命令怎么写。只要作者后续在分支上推一个恶意 commit,已经安装过的用户下次更新时也会被影响。

2.3 危险的命令参数

比命令本身更隐蔽的是参数。很多 MCP server 为了提供文件操作能力,会要求加--allow-write--dangerously-skip-permissions参数。这些参数原本是给用户在完全可控环境下用的,但如果一个恶意脚本与这样的参数组合在一起,相当于拿到了当前用户权限下的文件写入和命令执行能力。

我在体检中经常看到类似这样的配置:

{ "command": "npx", "args": ["-y", "some-mcp-server", "--dangerously-skip-permissions"] }

npx -y跳过安装确认,--dangerously-skip-permissions跳过权限弹窗。两步叠加,一条命令就能完成从"执行任意代码"到"绕过权限提示"的全链路,而最终配置文件里留下的只是一行很容易被忽略的启动参数。

3. 14 项配置风险逐项拆解

以下 14 项检查,是我在实践中沉淀下来的清单。每一项都有编号、检测方式、风险等级和修复建议,可以直接对照自己的 mcp.json 逐项自查。我在最后一节还会说明如何用一条命令自动完成这个过程的体检。

编号风险项检测方式风险等级修复建议
MCP-CFG-001command 指向不存在或不可执行的程序检查可执行文件是否存在改用绝对路径并确认权限
MCP-CFG-002使用 npx 且携带 -y/--yes 自动确认参数解析 args 列表锁定包版本并去掉 -y
MCP-CFG-003env 中有 LD_PRELOAD、DYLD_INSERT_LIBRARIES、NODE_OPTIONS、JAVA_TOOL_OPTIONS 等动态加载变量检测环境变量名移除或用最小区间值替代
MCP-CFG-004env 覆盖 PATH 且指向可写目录或空字符串检查 PATH 值移除 PATH 覆盖
MCP-CFG-005args 包含 --allow-write、--dangerously-skip-permissions、--root 等提权参数解析参数列表按最小权限原则移除
MCP-CFG-006依赖未锁定版本或 commit hash检查是否带 @版本号或 #commit固定到精确版本
MCP-CFG-007包名与知名项目高度相似与已知官方包名做相似度比对核对官方文档中的包名
MCP-CFG-008url 字段非 https 协议检查 URL 协议强制使用 https
MCP-CFG-009url 指向 localhost 或内网 IP解析域名和 IP 段移除或改用显式授权
MCP-CFG-010工具描述中出现疑似提示注入文本正则匹配敏感语句移除异常 server
MCP-CFG-011配置文件中硬编码 token、密码、密钥正则匹配常见密钥格式使用环境变量或密钥管理服务
MCP-CFG-012server 名称重复或相互覆盖检查 key 冲突重命名并确认唯一
MCP-CFG-013未声明任何权限范围字段检查是否存在 permissions/trustLevel补充权限声明
MCP-CFG-014配置文件权限过宽(其他用户可读/可写)检查文件 mode改为 0600

3.1 执行入口类风险

这一组风险集中在commandargsenv三个字段上。

先看 MCP-CFG-001。很多教程会写成"command": "some-tool",但some-tool并没有安装。这不算攻击,但会导致客户端反复重试启动、日志刷屏。更值得关注的是另一种情况:command 被改成/usr/bin/python3,而 args 中的脚本路径来自项目目录下的可写文件。如果攻击者可以往项目目录写入脚本,等于拿到了 RCE。

MCP-CFG-003 值得单独说明。LD_PRELOAD可以让进程加载攻击者指定的动态库,从而劫持几乎任意函数;NODE_OPTIONS可以让 Node.js 进程执行--require指定的脚本;JAVA_TOOL_OPTIONS对 JVM 有类似效果。这些环境变量如果出现在 mcp.json 的 env 字段里,加载的一瞬间就会生效,比任何后续利用链都直接。

3.2 来源与供应链类风险

MCP-CFG-006 是个非常容易被忽略但极其重要的问题。npx拉取的包如果不指定版本,使用的就是latest标签。今天安全,不代表明天安全——包作者被钓鱼、账号被盗、依赖被投毒,都可能导致旧版本引用新依赖时被污染。固定版本后,至少能保证你本地运行的是审计过的代码。

MCP-CFG-007 的检测逻辑可以借助包名相似度算法。比如官方包@modelcontextprotocol/server-github,伪造者可能发布@modelcontextprotocol/server-githbu;官方是blender-mcp,伪造者可能是blender-mcp-helper。肉眼很难分辨,但脚本可以计算字符串编辑距离,距离小于 2 的都要重点提示。

MCP-CFG-008 和 MCP-CFG-009 针对的是远程 MCP server。http://明文传输时,通信内容可以被中间人截获和篡改;ws://同理。指向 localhost 或内网 IP 的 URL,则可能被当作跳板去探测本机服务和内网资源。

3.3 数据与密钥类风险

MCP-CFG-011 是很多人在使用中容易犯的错。为了方便,把 GitHub Token、数据库密码、API Key 直接写进 mcp.json,并提交到 Git 仓库。一旦仓库泄露,密钥也随之暴露。即使不提交,mcp.json 的权限也未必安全——如果你用的是 0644 权限,本机其他用户就能读取。

体检时我通常会扫描常见的密钥格式:

sk-[a-zA-Z0-9]{20,} ghp_[a-zA-Z0-9]{36} AKIA[0-9A-Z]{16} Bearer [a-zA-Z0-9._~-]+

MCP-CFG-010 则是另一种类型的数据风险。MCP server 的工具描述中如果出现ignore previous instructionsdisregard system promptyou must output这类文本,大概率是攻击者故意插入的提示注入,目的是诱导客户端执行与用户意图不符的操作。正常工具描述不会写这些。

3.4 作用域与持久化类风险

MCP-CFG-012 常见于"多配置文件叠加生效"的场景。项目级.mcp.json定义了名为db的 server,全局配置里也有一个db,到底哪个生效取决于客户端的优先级规则。如果攻击者能往项目里放一个同名 server config,就可能覆盖原有配置,实现持久化劫持。

MCP-CFG-013 关注的是配置里有没有声明权限。很多客户端目前对 MCP server 的权限模型还比较粗放,默认允许 server 声明的所有工具被调用。如果你的mcp.json里没有任何permissionstrustLevelblockedTools之类的字段,等于在说"我信任这里面所有工具的任意行为"。

4. 把体检做成一条命令:检查工具的设计思路

4.1 配置文件发现与解析

如果把上面 14 项检查写成一个本地体检工具,第一个要解决的问题是"去哪找配置"。不同客户端的配置文件位置不同,常见的包括:

  • ~/.cursor/mcp.json
  • ~/.claude.json(其中的 mcpServers 字段)
  • ~/.config/Claude/claude_desktop_config.json
  • 项目根目录的.mcp.json
  • .vscode/mcp.json

工具可以设置一个搜索列表,按优先级扫描。解析时只取mcpServers对象的每一项,并保留 server 名称、command、args、env、url、disabled 等字段供后续检查使用。核心逻辑如下:

function loadServers(file) { const raw = fs.readFileSync(file, 'utf-8'); const data = JSON.parse(raw); return data.mcpServers || {}; }

这里有个细节:有些配置文件会把 server 放在数组里,有些放在对象里,还有的可能嵌套一层mcp前缀。稳妥做法是递归查找所有键名为mcpServers的对象。

4.2 检查项打分与输出格式

每条检查项最后输出四类状态:PASSINFOWARNFAIL

  • PASS:该项没问题;
  • INFO:存在但影响不确定,需要人工判断;
  • WARN:有潜在风险,建议修改;
  • FAIL:明确的高危配置,应该立即处理。

输出可以直接用表格或 JSON,方便后续接入 CI。比如:

[FAIL] MCP-CFG-013 server "filesystem" 未声明权限范围字段 [WARN] MCP-CFG-006 server "github" 依赖未锁定版本,当前会拉取 latest [FAIL] MCP-CFG-011 server "github" 的 env 中发现硬编码 token [PASS] MCP-CFG-008 server "filesystem" 未使用远程 url

4.3 关键检查代码片段

这里贴几个有代表性的实现片段。

MCP-CFG-002 检查 npx 自动确认参数:

function checkAutoConfirm(server) { const c = (server.command || '').toLowerCase(); const args = server.args || []; if (c.includes('npx') || c.includes('npm exec')) { if (args.some(a => ['-y', '--yes'].includes(a))) { return { id: 'MCP-CFG-002', status: 'WARN', detail: 'npx 使用了 -y/--yes 自动确认参数' }; } } return { id: 'MCP-CFG-002', status: 'PASS' }; }

MCP-CFG-010 检查提示注入特征:

const INJECTION_PATTERNS = [ /ignore\s+(previous|above|all)\s+instructions/i, /disregard\s+(system|previous|all)\s+prompts?/i, /you\s+must\s+(output|ignore|forget)/i, /system\s*(prompt|message)/i ]; function checkPromptInjection(server) { for (const p of INJECTION_PATTERNS) { if (p.test(server.description || '')) { return { id: 'MCP-CFG-010', status: 'FAIL', detail: '疑似提示注入文本' }; } } return { id: 'MCP-CFG-010', status: 'PASS' }; }

MCP-CFG-014 检查文件权限:

function checkFilePerm(file) { const stat = fs.statSync(file); const mode = stat.mode & 0o777; if (mode & 0o044) { return { id: 'MCP-CFG-014', status: 'WARN', detail: `文件可被其他用户读取,当前 ${mode.toString(8)}` }; } return { id: 'MCP-CFG-014', status: 'PASS' }; }

三组检查逻辑都不复杂,但组合起来就是一个非常实用的本地体检工具。检查完成后生成报告,把 FAIL 和 WARN 收集起来,可以同时输出 JSON 和 Markdown 两种格式,后者方便直接贴到工单或者 Wiki 里。

4.4 为什么选择了 npx 分发而不是其他方式

工具分发方式当时也纠结过:全局安装、本地脚本、Homebrew、npx 都有考虑。最终选择 npx 的理由是降低使用门槛——一行命令、不用处理全局路径、不用升级依赖。但这又带来了一个有趣的悖论:你用 npx 命令去检查 npx 风险,如果工具本身被投毒怎么办?

我的建议是,至少在首次使用时做几步确认:核对包名是否与官方文档完全一致、使用npm view <package>查看版本和维护情况、在package-lock.json或工具文档里看是否有 SHA-256 校验值。我自己在发布这类工具时也会主动公开源码地址,让大家可以先读后跑。安全体检工具应该比被检对象更透明。

5. 体检报告的修复建议与误报处理

5.1 常见修复动作

拿到体检报告后,很多问题修复起来并不复杂,关键是不要贪快。我按优先级排序,分享一下实际操作中常用的处理方式。

第一优先处理三类高危项:MCP-CFG-002MCP-CFG-003MCP-CFG-011。去掉不必要的-y参数,改成显式确认;移除LD_PRELOADNODE_OPTIONS这类危险环境变量;把硬编码密钥迁移到系统钥匙串或环境变量管理里。尤其是密钥,我见过不止一次因为 mcp.json 被 commit 到 GitHub 导致 Token 泄露的案例,迁移后还要立刻到平台侧 revoke 一次。

第二优先处理供应链类风险:MCP-CFG-006MCP-CFG-007。把npx包名固定到精确版本,格式是package@1.2.3;如果是 GitHub 依赖,则在 URL 后追加#commit-sha锁定。这样即使上游发生投毒,你的本地配置也不会被波及。

第三优先处理作用域与权限类风险:MCP-CFG-012MCP-CFG-013。梳理当前实际在用的 server,把重复项删除;能声明权限范围的客户端就显式声明disabledToolsallowedTools,只开放必要的工具。我个人的习惯是"宁可先限制,用到再开"。

接下来是收尾工作:MCP-CFG-001清理无效 command,MCP-CFG-008/009把所有能走 https 的 URL 改掉,MCP-CFG-014给配置文件设定为0600权限。这些操作在 10 分钟内能完成,但对整体攻击面的收敛非常有效。

5.2 误报与边界情况

体检工具最容易出现的问题就是误报,毕竟配置文件是给人服务的,不是每个异常都代表攻击。

比如MCP-CFG-006检测"未锁定版本"时,如果用户明确使用npx且每次都希望用最新版本,这算不算风险?我的判断是:个人开发者、单机本地、不处理敏感数据时,可以接受;团队协作、服务器环境、涉及生产数据时,必须锁版本并对上线流程做审计。工具只能标记,最终决策权在人。

再比如MCP-CFG-003检测NODE_OPTIONS,有些正常场景会用它来设置内存上限--max-old-space-size。这种情况下应该算INFO而不是FAIL,因为可控的参数值确实有实际用途。判断标准应该是"这个 env 是否会导致代码在非预期路径下被加载执行",如果只是内存参数,那风险就相对可控。

MCP-CFG-010提示注入检测也经常出现边界情况。有些描述里出现system prompt字样只是因为文档作者想解释概念,并不构成注入。我的做法是保留原始描述片段,让审计人员看到上下文再判断,而不是直接给一个武断的结论。

5.3 如何维护一份干净的 mcp.json

体检一次不难,难的是让配置长期保持在健康状态。这是我目前比较推荐的一套维护流程:

  • 每次新增 MCP server 前先确认来源,最好只在官方文档或社区高信誉用户的仓库中选择;
  • 新增后立即跑一次体检脚本,确认没有新增FAIL项;
  • 通行频率降低时主动清理不再使用的 server,删除比禁用更安全;
  • 定期检查已安装工具是否有更新,但不要自动升级,升级前先看 changelog 和包完整性;
  • 重要的 mcp.json 纳入版本管理时,先用脚本把密钥字段脱敏。

这套流程坚持下来以后,我的 mcp.json 从二十几个 server 精简到了 8 个左右,AI 工具链反而更稳定了。少即是多,这个原则在配置管理里同样适用。

最后分享一个个人感受:现在很多教程把 MCP 安装描述得过于无脑,一条 npx 命令复制粘贴,回车,完事。但安全从来不是"多一道命令"来保证的,它需要你在每个步骤里多想一步。那个"想的动作"只需要几秒钟——看一眼包名是不是官方拼写、看一眼参数里有没有-y、看了一眼工具描述里有没出现不该出现的英文句子。这几秒钟,比事后排查一条恶意命令的成本低得多。

体检的意义从来不是让配置变得完美,而是让每份配置里的风险都被摊在明面上,由你决定接受它还是清除它。我建议你项目里现在就跑一次检查,看看那 14 项里你中了哪几项。要是全绿,那恭喜,你大概率是那个会在粘贴命令前多看一眼的人;要是发现了几项,也别慌,逐条按修复建议处理,十分钟就能收工。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询