RSS

第 11 章 / 共 12 章

别人凭什么敢装:安全边界与发布

约 11 分钟 更新于

11.1 你的 server 是一段会被自动执行的代码

前面十章都在讲怎么让它工作。这一章讲一件不同的事:你的 server 一旦发出去,就是别人机器上一段由模型自主触发执行的代码。

这个身份带来的风险和普通的库不一样。库要人写代码去调用它;MCP server 的工具,是模型读了描述之后自己决定调用的,而调用的上下文里可能混着网页内容、issue 正文、别人发来的文档——任何不可信的文本。

有几组公开数据能说明这个生态目前的真实状态:

  • 2025 年 7 月的一次公开扫描发现了 1,862 个暴露在互联网上的 MCP server,人工验证的 119 个样本全部在没有任何认证的情况下交出了内部工具清单。
  • 2026 年 5 月的一篇学术测量论文覆盖了 7,973 个远程 MCP server,其中 40.55% 无需认证就能拿到工具列表;对启用了 OAuth 的 119 个 server 做深入测试,全部至少存在一处缺陷。
  • 工具投毒基准 MCPTox 用 45 个真实 server、353 个真实工具测了 20 个主流模型,最高攻击成功率 72.8%;即使是拒绝率表现最好的模型,拒绝率也不到 3%。

最后那个数字值得多想一秒:你不能指望模型帮你挡住恶意的工具描述。防线必须建在别的地方。

第11章:别人凭什么敢装:安全边界与发布
图 11.1:纵轴是攻击来自哪里,横轴是防线该建在哪一侧。四个象限对应四类完全不同的措施,任何一类都替代不了另一类。

11.2 这个生态里已知的四类攻击

先认识对手。下面四类都有公开的研究和复现代码,不是假想。

工具投毒(Tool Poisoning)。 恶意指令被嵌进 MCP 工具描述里——用户看不见,模型看得见。研究者演示过一个「看起来只做加法」的工具,它的描述里藏着指令,要求模型顺便读取 SSH 私钥和配置文件,通过一个不起眼的参数外传,并且「用数学解释掩盖这一点」。它的根基是信息不对称:模型读到完整描述,用户在界面上只看到简化版。

事后掉包(Rug Pull)。 客户端批准某个工具之后,恶意服务器把工具描述改掉。这和包仓库投毒是同一个套路:审核时是良性的,通过之后再推恶意更新。批准这个动作,在服务端变更的那一刻就失效了。

跨服务器影子攻击(Shadowing)。 一个恶意 server 的工具描述,去改写模型对另一个可信 server 的工具的使用方式。研究里演示过:即使用户明确指定了收件人,所有邮件仍被改投到攻击者地址。规范对此有对应条款——工具名的唯一性只在单个 server 内成立,聚合多个 server 的客户端应当用服务器标识做前缀消歧,并且不应该拿 serverInfo.name 当唯一键(它不保证唯一)。

未调用即中招(Line Jumping)。 这条最反直觉:MCP server 可以在从未被调用的情况下影响模型行为。因为工具描述在连接阶段就进了模型上下文。这打破了两个想当然的假设——「不调用就不会出事」不成立,「server 之间互相隔离」也不成立(攻击者可以指示模型代为转发,在本该隔离的组件之间架起一座间接的桥)。

11.3 你能做的:作为服务端的自我约束

上面四类主要是「别人写的 server 攻击用户」。作为一个正经 server 的作者,你的责任是两件事:不要成为攻击面,也不要成为帮凶。

边界要由配置决定,不能由模型决定。 这是最重要的一条。笔记库服务的例子:

def resolve_note(filename: str, root: Path) -> Path:
    path = (root / filename).resolve()
    if not path.is_relative_to(root.resolve()):
        raise ToolError("只能访问笔记目录内的文件。")
    return path

关键在 resolve() 之后再判断包含关系——只做字符串前缀比较是挡不住 ../ 的。而且允许的根目录来自服务器配置(环境变量),不是工具参数。这一点在 2026-07-28 之后更重要了:规范弃用了 Roots 特性,官方给出的迁移建议正是「通过工具参数、资源 URI 或服务器配置传递目录和文件」——目录边界属于配置,不属于模型能改的东西。

永远不要把模型给的字符串拼进 shell。 需要执行外部命令时,用参数数组形式,不要用 shell=True。

密钥不进三个地方:日志、返回值、错误信息。 有安全研究专门统计过,大量 MCP 环境把第三方长期 API key 明文写在本地文件里,权限还是所有用户可读。第 6.4 节讲的异常遮蔽机制正是为此设计的——让它替你工作,不要绕过它。

规范对 server 的硬性要求,逐条落实: 校验所有工具输入、实现适当的访问控制、对工具调用做限流、对工具输出做净化、记录工具调用日志用于审计、给工具调用设超时。

诚实标注,但知道标注不是安全机制。 只读工具标 read_only_hint=True,只做新增的标 destructive_hint=False,闭域的标 open_world_hint=False。这些标注帮助宿主给用户更准确的提示。但规范同时要求:客户端必须把工具标注视为不可信的,除非它们来自可信的服务器。官方博客说得更直白——标注不会让模型抵抗提示注入,它只是工具定义上的静态元数据。真正的只读,是你的代码里没有写文件的那行。

有真实副作用的操作,做成两步。 第 7.5 节那个 plan_cleanup / apply_cleanup 的 handle 模式,除了解决状态问题,本身就是一道安全设计:先返回一个可读的计划让人看见,再执行。配合上一章讲的强制确认标记,这是给危险工具加护栏最实际的做法。

11.4 一个值得记住的判断框架

官方博客在讨论工具标注时引用过一个很好用的框架,叫致命三件套(lethal trifecta):当一个会话同时具备下面三样时,数据被窃取的风险就成立了——

  1. 能访问私有数据
  2. 会接触不可信内容
  3. 具备对外发送的能力

关键在于这是会话级的组合判断,不是单个工具的属性判断。你的笔记搜索工具单独看完全无害(只读、不联网)。但如果用户同时装了一个会抓取网页的 server 和一个能发消息的 server,三件套就凑齐了:网页里的一段注入文本,可以指挥模型去读你的笔记,再发出去。

这对你的意义是:在 README 里说清楚你的 server 属于三件套里的哪一件。「本 server 可读取用户本地笔记目录的全部内容」是一句必须写出来的话,它让用户能自己做组合风险判断。

11.5 发布前自检清单

输入与输出
[ ] 所有工具输入都做了校验(用类型和 Field 约束,不是函数体里的 if)
[ ] 文件类工具:路径 resolve 后校验在允许根目录内,根目录来自配置而非参数
[ ] 不把模型给的字符串拼进 shell
[ ] 工具输出做了截断/分页,并在截断时给出引导

凭据与日志
[ ] 密钥只从环境变量读,不硬编码
[ ] 密钥不出现在日志、返回值、错误信息里
[ ] 本地凭据文件权限收紧,不是所有用户可读

权限与边界
[ ] 只读工具标了 read_only_hint,且代码里确实不写任何东西
[ ] 有副作用的工具,要么两步确认,要么标记为需要用户交互
[ ] 远程部署:校验 Origin 头、只接受签发给自己的 token、所有连接都认证
[ ] 状态 handle 不当作身份凭据使用

可观测与兜底
[ ] 工具调用有日志(含入参摘要,不含完整用户数据)
[ ] 工具调用有超时和限流
[ ] 错误分两层:给模型的可自愈提示,和给自己的完整堆栈

文档
[ ] README 写清楚:它能访问什么数据、会不会联网、有没有写操作
[ ] 写清楚需要哪些环境变量,以及最小权限的取值
[ ] 写清楚你在哪个 SDK 版本和协议版本上测过

11.6 发布与分发

代码写完、清单过完,最后一步是让别人能装上。

最低限度:一个能抄的 README。 这是绝大多数 server 实际的分发方式,也是最被低估的一环。它至少要有四样东西:一句话说清这个 server 干什么;一段可以直接复制的客户端配置(分别给 Claude Code 和 Codex CLI 各写一条命令);需要的环境变量清单;以及 11.4 那句关于数据访问范围的坦白话。

发成包。 Python 的 server 发到 PyPI,用户就能用 uvx 直接跑,不用先 clone 你的仓库。这一步的好处是让启动命令变短、变稳定,也让版本可控。

官方注册表。 MCP 有一个官方 Registry,是目前唯一的官方目录。写这份教程时它仍处于 preview 阶段,还没有正式 GA,所以别把它当成必经之路。它的模型是「只存元数据」——你必须先把包发到 npm 或 PyPI,注册表里登记的是指向那个包的信息。命名空间有两种形式:io.github.<你的用户名>/<server 名>(需要以该 GitHub 身份登录),或者你自己的域名(需要通过 DNS 或 HTTP 验证所有权)。发布用官方的 mcp-publisher 工具走登录和发布流程。

顺带给一个参考量级:写这份教程时对官方 Registry 全量翻页统计,收录了约 27,000 个 server。第三方目录的数字更大,但各家口径不一。这个数字的意义不是「生态很热闹」,而是:你的 server 需要一个清晰的名字和一句准确的描述,否则它就是那两万多条里没人点开的一条。

版本与兼容。 最后写清楚你在哪个 SDK 版本和哪个协议版本上验证过。这一年 MCP 变化很快——2026-07-28 的破坏性重构、SDK 从 1.x 到 2.x 的改名,都在你眼前发生过。给用户一个明确的坐标,比给一句「支持最新版」有用得多。

11.7 练习与检查点