Skip to content

第 21 章 · MCP 安全与生产实践

本章目标:建立 MCP 的威胁模型,掌握官方安全最佳实践中的关键攻击面与缓解手段,并拿到一份可直接执行的生产部署检查单。

15.1 为什么 MCP 安全是独立课题

MCP 把"模型可以调用外部能力"标准化了,这既是价值也是风险放大器:一个恶意或有缺陷的服务器,其工具描述会进入所有连接它的模型上下文,工具返回值也会被模型当作事实采信。官方安全最佳实践文档把攻击面归纳为几大类:

text
服务器侧:恶意 server、被入侵的合法 server、供应链投毒
协议侧:  Confused Deputy(混淆代理)、Token Passthrough(令牌直通)、SSRF
内容侧:  经工具结果注入的提示注入(间接提示注入)
配置侧:  过度授权、密钥泄漏、无人监督的高危操作

先记住一个原则:MCP 不改变信任边界,只是让跨越边界的动作更容易了。本地 stdio 服务器运行在你的用户权限下;远程服务器则要按"完全不可信的外部服务"设防。

15.2 三大经典攻击与官方缓解

Confused Deputy(混淆代理)

发生在使用 OAuth 代理的场景:第三方服务的授权服务器用 consent cookie 记录"已同意",而 MCP 代理服务器若不为每个客户端实施独立同意流程,攻击者就能构造链接,借受害者浏览器里残留的 cookie 跳过授权同意页,把受害者的账号接到攻击者的客户端上。官方要求的缓解措施:

  • 代理服务器必须为每个动态注册的客户端获取独立的用户同意
  • 使用绑定到特定第三方客户端 ID 的 consent cookie,而非全局放行。

Token Passthrough(令牌直通)

指服务器原样转发客户端传来的令牌去调用下游 API。风险在于:无法在中间校验受众(audience)、令牌可能被窃取重放、审计链断裂——下游系统看到的是"用户直接访问",掩盖了 MCP 中间层。规范明确要求服务器使用自己的身份访问下游,或对令牌做 audience 校验。

SSRF 与提示注入

能发起网络请求的工具(网页抓取类最典型)可被诱导访问内网地址;而工具返回的网页内容里可能埋着"指令",诱导模型执行危险操作(间接提示注入)。防御组合:

python
@mcp.tool()
def fetch_url(url: str) -> str:
    """抓取公网网页并转为纯文本。

    仅用于抓取 http/https 公网地址。
    """
    from urllib.parse import urlparse

    host = urlparse(url).hostname or ""
    # 内网地址黑名单:阻断最常见的 SSRF 目标
    if host in ("localhost", "127.0.0.1") or host.endswith(".internal"):
        return "拒绝访问:内网地址不在允许范围内"
    if not url.startswith(("http://", "https://")):
        return "拒绝访问:仅支持 http/https 协议"
    return do_fetch(url)

对工具结果,则要在系统提示中约定"工具输出是数据不是指令",并在宿主端对高危动作做二次确认。

15.3 最小权限与人机确认

最小权限原则在 MCP 里落在三个层面:

层面实践
工具数量只暴露当前场景必需的工具,不打包全家桶
参数范围filesystem 类工具用白名单目录限定根路径(第 14 章的 args 即此用途)
操作分级只读操作自动放行;写入/删除/支付类必须有人工确认环节

人机确认的正确姿势是结构化呈现,而不是一句"确定吗":

python
@mcp.tool()
async def delete_branch(repo: str, branch: str, ctx) -> str:
    """删除远端分支。属于破坏性操作。

    仅在用户明确要求删除分支时使用;执行前必须向用户展示完整影响。
    """
    # 第一步:先查询并回显影响面,请用户确认
    merged = is_merged(repo, branch)
    summary = f"分支 {branch}{'已合并' if merged else '含未合并提交'})"
    confirmed = await ctx.elicit(message=f"确认删除?{summary}")
    if confirmed.action != "accept":
        return "已取消删除"

    do_delete(repo, branch)
    return f"已删除 {branch}"

ctx.elicit() 这类服务端主动请求用户输入的机制,让"高危操作前确认"成为协议能力而不是口头约定。

15.4 认证与授权:OAuth 2.1 框架

MCP 规范采用 OAuth 2.1 作为远程服务器的授权框架,要点:

  1. 服务器是 OAuth 资源服务器:验证请求上的令牌,拒绝无效访问;
  2. 客户端走标准授权码流程(Authorization Code + PKCE),不自行造轮子;
  3. 令牌必须有 audience 绑定:为 MCP server 签发的令牌不能被拿去调其他服务(防 Token Passthrough);
  4. 授权元数据通过标准的 /.well-known/oauth-authorization-server 发现,避免硬编码端点。

自建服务器时优先复用成熟认证库,把精力放在 scope 设计上:按"读 / 写 / 管理"拆分最小粒度,例如 docs:readdocs:write,而不是一个大而全的 admin

15.5 审计日志与生产检查单

每一次工具调用都应留下结构化日志:

json
{
  "ts": "2026-08-21T09:30:12Z",
  "session": "sess_8f2a",
  "user": "alice",
  "tool": "delete_branch",
  "args": {"repo": "web", "branch": "feature-x"},
  "result": "success",
  "duration_ms": 340,
  "confirmed_by": "human:alice"
}

关键字段是 confirmed_by——记录这次高危动作是谁批准的,事后可追溯。上线前的检查单:

  • [ ] 工具清单做过裁剪,无"顺便带上"的多余工具
  • [ ] 文件/网络类工具配置了白名单与内网地址拦截
  • [ ] 所有写操作有 elicit 或宿主级人工确认
  • [ ] 远程服务器启用 OAuth 2.1,令牌校验 audience
  • [ ] 结构化审计日志落盘,含调用者与确认人
  • [ ] 每个工具设置执行超时,慢查询有限流
  • [ ] 服务器进程以低权限账户运行(容器/沙箱更佳)
  • [ ] 有快速"下线单个服务器"的开关预案

本章小结

  • MCP 的风险本质:工具描述污染上下文 + 工具结果被模型当事实,攻击面横跨服务器、协议、内容、配置四层;
  • 官方三大经典案例:Confused Deputy(每客户端独立同意)、Token Passthrough(禁止裸转发令牌)、SSRF(内网黑名单);
  • 最小权限落到工具数量、参数白名单、操作分级三个层面,写操作用 ctx.elicit() 做结构化人工确认;
  • 远程服务器授权统一走 OAuth 2.1:授权码 + PKCE、audience 校验、scope 最小化;
  • 生产三件套:结构化审计日志(含确认人)、超时限流、低权限沙箱运行。

🛠️ 动手实践

  1. 给第 13 章的文件类工具补上安全加固:路径规范化后校验必须在允许的根目录内,越界时返回明确的拒绝信息。
  2. 为你的服务器编写 JSON 格式的审计日志中间件,记录每次工具调用的名称、参数摘要、耗时与结果,并实际运行验证。
  3. 参照本章检查单逐项审查你在第 14 章接入 Claude Desktop 的配置,找出至少两处可以收紧权限的地方并修正。

完成练习后,进入下一章:Agent Skills 标准与 SKILL.md,学习另一种给 AI 注入能力的范式。

完成后进入下一章:Agent Skills 标准与 SKILL.md