首页 / 文章

检查了多少代码量?对300个MCP npm包进行了审计,未发现任何问题。

对npm上300个MCP服务器包进行的压缩包审计发现,有217个包的运行时代码从未被扫描工具读取,因此即便审计结果看似正常,也需要附上代码覆盖率证明。

发布者:REACTAPP.TOP阅读约 8 分钟1871 词

来源: I Audited 300 MCP-Related npm Packages. My Scanner Couldn’t Inspect Runtime Code in 217. So I created Driftward.

共有300个名称中包含MCP的npm包从注册表中被提取出来,并以发布的tarball形式进行了检查,但从未实际安装或运行过。最引人注目的结果与恶意软件数量无关,而是一种覆盖率缺陷:在300个包中的217个(72.3%)中,tarball中确实存在运行时代码,但这些运行时文件都位于扫描工具会刻意跳过的路径下,比如dist/、build/或压缩后的输出文件所在目录。该工具能够读取manifest文件和README文件,给出看似正常的检测结果,却根本不会查看那些真正会执行的代码。

具体而言,有217个包存在覆盖率缺陷,80个包含有合格的运行时代码,还有3个包根本找不到任何运行时代码。

这并不意味着72.3%的MCP包完全无法被静态分析工具检测到。这些打包文件是可以被解析的,源代码映射有时能够还原原始代码,而专门的工具则可以反混淆处理或分析生成的JavaScript代码。这一结论更为具体且实用:

除非同时说明实际检查了多少可执行代码,否则负面的静态分析结果没有任何意义。

范围在这里很重要。该结果仅表示某款扫描工具在217个tar文件中未发现任何可读取的内容,并不能说明这些包具有恶意或不安全,也不能表明其他分析工具无法检测它们。

为何MCP会提升风险等级

本地MCP服务器并非仅仅是应用程序导入的库。在常规的stdio传输方式下,客户端会以子进程的形式启动它。该项目的安全策略明确规定,服务器拥有与客户端相同的系统级权限,且SDK的stdio传输方式不提供沙箱保护。如果代理能够访问到凭证、网络地址或本地工具,那么没有沙箱保护的服务器很可能也能访问到它们。

官方的MCP安全指南建议以最低权限启动本地服务器,并限制其对文件系统及网络的访问。能否实现这种隔离则取决于主机或操作员。代码审查依然具有价值,但问题在于它究竟能真实反映开发者实际安装的组件情况。

示例的构建方式

这只是一个便于使用的示例,并非对整个生态系统的全面调查。

该收集工具使用了“mcp server”、“model context protocol”、“mcp connector”以及“modelcontextprotocol”等关键词,对npm注册表进行了11次搜索。每次查询最多返回100条结果,占三页的篇幅;它会剔除重复项,仅保留包含“mcp”的名称,并将结果数量限制在300条以内。搜索结果的排序决定了哪些项目会被纳入,同一个发布者可能多次出现,而包含“mcp”的名称并不代表该项目一定是MCP服务器。

对于每个软件包,系统会确定收集时的最新版本,直接下载注册表的压缩文件,验证npm提供的完整性值,并将相关内容加载到内存中。所有300次完整性检查均通过,且没有出现下载或解析错误。最终没有任何内容被安装、解压到可执行环境中,也没有被运行。

该扫描器经过优化以提高精确度。它会查找网络 API、具体的 URL 主机地址、子进程调用、凭证路径的提及以及有限范围内的注入语句,同时忽略测试代码、固定配置、示例、编译后的输出文件夹、压缩文件、源映射以及大小超过 1 MB 的任何内容。这些忽略机制虽能减少干扰,但也会产生可量化的检测盲点。

检测结果

覆盖率应与发现的问题一同呈现,而非放在脚注中。结果的第一行即为主要发现,其余每一行都需要结合第一行内容来理解。例如,若未检测到任何凭证相关内容,并不意味着代码库中从未出现过凭证信息,而只是说明扫描器在它认为符合条件的代码中未找到相关内容,尽管有 217 个包并未提供任何可供扫描的代码。

有四个数据尤为突出:

  • 存在运行时代码但无符合条件的运行时文件:217 个包。
  • 在这 217 个包中,107 个包含源代码映射文件,其余 110 个则没有。
  • 符合条件的代码中存在网络 API 或直接主机相关信号:50 个包。
  • 呈现注入式特征的匹配项有 2 个,经人工审核后均被判定为误报。

两起误报

有两个包中出现了注入式短语,经人工核查后发现均为说明性文本。其中一个是版本为 1.2.22 的 Mastra 文档服务器,其中包含类似“IGNORE PREVIOUS INSTRUCTIONS”这样的评估示例;另一个是版本为 0.1.71 的 REMnux MCP 服务器,它警告恶意软件样本可能含有注入式字符串,并告知分析人员应如何处理这些内容。

同样也没有试图操纵代理的迹象。这是关键词扫描的一个已知缺陷:安全文档、测试用例以及恶意指令可能包含完全相同的字符串,只有结合上下文才能区分它们。

评分为零的恶意测试包

有一个测试包被用作覆盖率控制工具。malicious-mcp-server 1.5.0自称是一个专为端到端测试设计的恶意MCP服务器。它的压缩包中包含三个文件:大小为970字节的package.json、大小为742字节的README.md,以及大小为610,358字节的dist/index.js。

初次扫描时,系统读取了这两个较小的文本文件,跳过了dist/index.js,未发现任何行为特征,因此给该测试包的风险评分定为0。之后对跳过的文件进行原始标记统计后,发现其中出现了6次exec、2次eval、52次base64以及3次raw.githubusercontent.com。

因此,客观的结论并非一切正常。根据现行规则无法确定运行时的覆盖情况。那些字符串本身既不能证明程序已执行,也不能证明存在恶意行为,而包的清单则提供了缺失的上下文信息。问题在于,承载所有风险的程序包始终未能通过能输出零值的检测。

技术本身并无过错

省略生成的输出结果属于设计决策,而非该领域的规则要求。不同的扫描工具可能会解析打包后的 JavaScript 代码、检查语法树、解压模块、提取源码映射、追踪依赖元数据、比较代码来源,或是运行为压缩代码编写的规则。这些方法都会在速度和精确度上做出一定牺牲,以获得更全面的检测能力。

错误在于将所有检测结果视为单一条件,因为实际上它可能代表至少三种不同的情况:

  1. 已读取相关代码,且没有触发任何规则。
  2. 已读取相关代码,但规则无法识别该行为。
  3. 从未读取过相关代码。

只有第一种情况才是有意义的正常结果,即便如此也不能作为安全性的证明。

一份好的报告应披露哪些内容

扫描工具不应将“未匹配项”与“未检查项”合并为同一个绿色结果。对于软件包的生成文件,每份问题汇总都应附上覆盖率说明:

  • 存在多少运行时文件,分析了多少,覆盖了多少字节。
  • 所有被排除的项目,按原因分类:生成的路径、大小限制、不支持的格式、解析失败等。
  • 源代码映射文件的存在情况,以及是否能够从中恢复真实的源代码。
  • 生成该结果的规则集及扫描工具版本。
  • 针对检查过的代码未发现问题与运行时覆盖率无法获取的情况,分别显示不同的结果状态。
  • 这虽只是报告方式上的微小改动,却能极大提升信任度,因为审查者可以判断安静的结果是真实证据,还是仅仅因为缺乏可见性而已。

    为何运行时行为依然重要

    静态审查仅能提供某一时刻的快照。软件更新后其行为可能会发生变化,而像 npx package-name 这样的非固定命令在本地 MCP 配置保持不变的情况下仍可能调用到新的代码。因此,运行时控制是静态审查的补充而非替代。每个本地服务器都应以独立身份运行,并遵循最小权限原则。其实际访问目标与功能必须被记录下来,且需明确批准基准配置。后续运行时会将该服务器的历史数据与之对比,这样一旦配置发生变化导致身份漂移,或者在不改变原有定义的情况下进行了软件包更新但运行时功能发生变动,都能被及时发现。

    有一种开源工具采用这种模式,将每个MCP服务器视为独立的主体,例如mcp:github,并为它配置单独的策略、签名后的基准值以及偏差记录。该工具旨在作为代码审查时的运行时保障措施,而非替代品。不同平台的支持程度不一:在macOS上,Seatbelt会将出站套接字绑定到回环代理;而在Linux上,bubblewrap目前仅提供文件系统与进程隔离以及代理环境变量功能,没有类似的网络绑定机制。

    最新的研究也在沿着相同方向推进。2026年8月发布的一篇预印本论文TrustShiftProbe模拟了那些起初表现正常、但在特定信任阈值点后改变行为的MCP服务器,并测试了基于行为基准的运行时防御机制。虽然预印本并非共识性成果,但其考虑时间因素的威胁模型是正确的,因为审查完成后信任度仍可能下降。

    审计的局限性

    • 样本是通过搜索排名和名称过滤筛选出的,因此既非随机也缺乏代表性。
    • 所有数据均反映2026年9月1日的状况,npm中的内容与搜索结果都会发生变化。
    • 统计的是压缩包数量,而非每个压缩包是否为可运行的MCP服务器。
    • “扫描器盲区”这一概念仅适用于该扫描器的排除规则,而非整体静态分析。
    • 虽然统计了源映射文件的数量,但并未检查每个映射文件是否包含可用的源代码。
    • 字面形式的URL和API名称仅属于静态线索,无法说明某条代码路径是否正在运行。
    • 这些软件包从未被实际执行过,因此运行时的网络目标地址仍然未知。
    • 未发现异常并不等同于安全通过判定。

    复现运行过程

    若要重建完全相同的压缩包集合,可使用冻结后的结果文件作为锁定依据:

    # self-bootstrapping — no install needed
    git clone https://github.com/abdalhafeezbushara/driftward.git
    cd driftward
    python3 research/mcp-npm-audit-2026-09-01/audit.py \
      research/mcp-npm-audit-2026-09-01/results.json \
      /tmp/mcp-audit-rerun.json
    

    要从当前的 npm 搜索结果中获取最新的 300 个包的快照:

    N=300 ./detonate/fetch-corpus.sh /tmp/mcp-packages.txt
    python3 research/mcp-npm-audit-2026-09-01/audit.py \
      /tmp/mcp-packages.txt /tmp/mcp-audit-current.json
    

    这两种方式都不会安装任何包或执行其代码;它们仅负责获取并读取 tar 文件。

    MCP 用户的简短检查清单

    • 锁定版本。使用精确的版本号及完整性值,比直接使用 npx 调用更可靠。
    • 查看已发布的成果。如果安装的 tar 文件中包含从未被打开过的生成代码包,那么再干净的仓库也毫无意义。
    • 询问代码覆盖率数据。如果报告显示没有符合条件的运行时文件,应视为该工具不可用而非代码干净。
    • 为每台服务器单独设置隔离机制,为其指定独立的身份、文件系统范围及允许访问的主机,无论其是在本地运行还是通过桥接方式运行。
    • 有意识地建立信任机制:记录一次已知正常的运行过程,对这一基准进行签名,一旦功能发生变化则阻止持续集成或要求重新审核。

    放弃静态分析并非关键所在,真正重要的是明确其检测覆盖率。对于MCP服务器以及代理程序使用的类似工具而言,读取源代码、分析构建后的产物、检查来源、进行沙箱测试、记录运行时行为以及检测偏差,这些功能各自对应着同一问题中的不同层面。安全报告中最令人担忧的表述未必是检测到恶意软件,而是“未发现任何问题”,这会引出下一个疑问:在多大范围的代码中均未发现问题?