ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

mcp.json安全体检:14项配置风险与npx供应链防护

mcp.json安全体检:14项配置风险与npx供应链防护 群里的消息我到现在还记得一个同事甩了条命令说装上之后 AI 能直接读数据库、搜代码、操作浏览器。不到五分钟办公室至少六个人复制粘贴运行了。新加入的 MCP 生态项目越来越多教程里超过一半的安装步骤都是这种模式——一条npx ... server命令自动生成配置文件然后重启应用。问题是几乎没有人打开过那个被自动创建出来的mcp.json看看里面到底写进去了什么。这篇文章想做的就是把mcp.json这个文件拉出来单独审一遍。它已经从一个普通配置文件逐渐变成客户端启动时的执行入口攻击面比大多数人想象的要大得多。我会先从为什么它危险讲起再逐项拆解本地体检时最值得关注的 14 项配置风险最后给出一条 npx 命令就能在本地完成检查的落地思路以及检查结果出来后怎么修、怎么判断误报。1. mcp.json 为什么突然变成了高危文件1.1 从一个配置文件到执行入口传统插件系统里配置文件一般只是声明我有哪些功能真正运行逻辑在已安装的插件包里。MCPModel 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.shargs字段可以携带-y、--allow-write这类自动确认或提权参数env字段可以注入LD_PRELOAD、NODE_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 gitgithub.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 列表高锁定包版本并去掉 -yMCP-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 协议高强制使用 httpsMCP-CFG-009url 指向 localhost 或内网 IP解析域名和 IP 段中移除或改用显式授权MCP-CFG-010工具描述中出现疑似提示注入文本正则匹配敏感语句中移除异常 serverMCP-CFG-011配置文件中硬编码 token、密码、密钥正则匹配常见密钥格式高使用环境变量或密钥管理服务MCP-CFG-012server 名称重复或相互覆盖检查 key 冲突中重命名并确认唯一MCP-CFG-013未声明任何权限范围字段检查是否存在 permissions/trustLevel中补充权限声明MCP-CFG-014配置文件权限过宽其他用户可读/可写检查文件 mode低改为 06003.1 执行入口类风险这一组风险集中在command、args、env三个字段上。先看 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 instructions、disregard system prompt、you must output这类文本大概率是攻击者故意插入的提示注入目的是诱导客户端执行与用户意图不符的操作。正常工具描述不会写这些。3.4 作用域与持久化类风险MCP-CFG-012 常见于多配置文件叠加生效的场景。项目级.mcp.json定义了名为db的 server全局配置里也有一个db到底哪个生效取决于客户端的优先级规则。如果攻击者能往项目里放一个同名 server config就可能覆盖原有配置实现持久化劫持。MCP-CFG-013 关注的是配置里有没有声明权限。很多客户端目前对 MCP server 的权限模型还比较粗放默认允许 server 声明的所有工具被调用。如果你的mcp.json里没有任何permissions、trustLevel、blockedTools之类的字段等于在说我信任这里面所有工具的任意行为。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 检查项打分与输出格式每条检查项最后输出四类状态PASS、INFO、WARN、FAIL。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 未使用远程 url4.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)\sinstructions/i, /disregard\s(system|previous|all)\sprompts?/i, /you\smust\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-002、MCP-CFG-003、MCP-CFG-011。去掉不必要的-y参数改成显式确认移除LD_PRELOAD、NODE_OPTIONS这类危险环境变量把硬编码密钥迁移到系统钥匙串或环境变量管理里。尤其是密钥我见过不止一次因为 mcp.json 被 commit 到 GitHub 导致 Token 泄露的案例迁移后还要立刻到平台侧 revoke 一次。第二优先处理供应链类风险MCP-CFG-006、MCP-CFG-007。把npx包名固定到精确版本格式是package1.2.3如果是 GitHub 依赖则在 URL 后追加#commit-sha锁定。这样即使上游发生投毒你的本地配置也不会被波及。第三优先处理作用域与权限类风险MCP-CFG-012、MCP-CFG-013。梳理当前实际在用的 server把重复项删除能声明权限范围的客户端就显式声明disabledTools、allowedTools只开放必要的工具。我个人的习惯是宁可先限制用到再开。接下来是收尾工作MCP-CFG-001清理无效 commandMCP-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 项里你中了哪几项。要是全绿那恭喜你大概率是那个会在粘贴命令前多看一眼的人要是发现了几项也别慌逐条按修复建议处理十分钟就能收工。
返回列表