MCP 服务器能跑通 demo 和能安全上线是两件事,授权、SSRF 与工具审批必须一起检查。
- 01先读摘要,判断是否与你的场景相关。
- 02再看来源,保留继续查证的路径。
- 03最后看步骤、风险和可复用动作。
MCP 服务器能跑通 demo 和能安全上线是两件事。本地工具可以靠用户确认挡住大部分风险,但一旦放到代理、团队环境或云端,OAuth 授权、redirect_uri、SSRF、state 校验和本地命令审批都要一起检查。下面这套清单按 MCP 官方安全最佳实践与授权规范排,适合从开发演示切到正式接入之前逐项过。
适用场景
- 你要把 MCP 服务器接入 Claude、ChatGPT、Codex 等客户端,或自建 MCP 代理。
- MCP 服务器会访问第三方 API、内部系统或执行本地命令。
- 你有多个用户或客户端实例,需要区分谁授权了哪个 scope。
- 服务器部署在公网、容器或共享网络上,不能只依赖桌面端人工确认。
不适用场景
- 你只在本地开发环境跑一个一次性工具,不处理多人授权和网络暴露。
- 工具本身没有权限边界,也还没有确定谁可以批准哪些动作。
- 你希望 MCP 层的授权替代第三方 API 的 OAuth 权限设计。
- 代理或服务器还没有日志、回滚和清理能力,直接上线只会让问题更难追踪。
先分清四类风险
MCP 官方安全文档把风险分成协议传输、授权流程、工具执行和客户端网络请求几类。授权文档则重点讲 OAuth、per-client consent、redirect_uri 和 state 参数。上线前不要只盯着“模型会不会说错话”,更常见的问题是:一个 MCP 客户端获得 token 后,能否访问它本不该访问的资源;一个恶意服务器能否让客户端请求内网地址;一个本地命令工具能否在没有审批时执行删除或写入操作。
准备材料
- MCP 客户端和服务器清单,包括传输方式、部署位置和用户范围。
- 第三方 API 的 OAuth 客户端注册信息,以及允许的 redirect_uri。
- 可用的 HTTPS 域名或隧道,不要在公网直接暴露明文 transport。
- 本地命令工具清单,标出哪些命令有写入、删除、外发或支付风险。
- 日志、告警和回滚方式。
步骤一:先确认传输与边界
MCP 支持 stdio、HTTP 和 Streamable HTTP 等传输方式。本地开发用 stdio 最直接,服务器端接入则要使用受控的 HTTP 入口,并优先 HTTPS。官方安全最佳实践明确要求生产环境中的 OAuth 相关 URL 使用 HTTPS;客户端在获取授权元数据时还应限制私网、保留地址和云 metadata 地址,避免 SSRF。
- 记录每个 MCP 服务器的 transport 类型和监听范围。
- 生产入口使用 HTTPS,并关闭不必要的目录、端口和通配路由。
- 在客户端或代理层限制出站目标,拒绝常见私网与 metadata 网段。
- 检查 DNS 解析结果是否会被重定向到内网地址,不要只做一次 IP 校验。
步骤二:按 per-client consent 设计授权
授权规范强调代理服务器必须维护每个用户的 client_id 注册表,并在发起第三方授权前检查该客户端是否已获得同意。consent 页面要清楚显示客户端名称、请求的第三方 API scope、redirect_uri,并实现 CSRF 保护。不能用“用户已同意这个网站”这样的全局状态代替 per-client 同意。
- 为每个 MCP 客户端分配独立 client_id,不要共用客户端身份。
- 授权前读取该用户的客户端注册表,未批准则先走 consent 页面。
- consent 页面使用 frame-ancestors 或 X-Frame-Options 防止点击劫持。
- 把 scope 列表、redirect_uri 和风险说明一起展示,不能只给一个“同意”按钮。
步骤三:把 redirect_uri 和 state 校验做成硬校验
OAuth 流程最容易出问题的是回调校验。官方要求 redirect_uri 必须与注册值完全匹配,state 要使用加密随机值、一次性使用、短过期,并在回调时精确比对。state 会话不能在用户同意之前就写入,否则攻击者可以用伪造授权请求绕过 consent。
- 在授权请求中生成新的 state,并只存到服务端会话。
- 回调时先校验 state 是否匹配且未使用,再处理授权码。
- 校验 redirect_uri 时使用完整匹配,不接受前缀匹配或通配。
- 登录失败或 state 不匹配时记录日志并返回错误,不继续后续流程。
步骤四:用 SSRF 防护约束客户端和授权服务器
MCP 客户端在发现服务器时可能从服务器提供的 URL 拉取 OAuth metadata,授权服务器也可能拉取 client metadata。两者都可能被恶意输入指向内网。官方建议不手工解析 IP,而使用成熟库、代理或网络策略;要同时检查重定向目标,不能只校验第一个 URL。
- 出站请求统一走 egress proxy 或 allowlist 代理。
- 禁止访问 loopback、私有 IPv4/IPv6、link-local 和云 metadata 地址。
- 对重定向再做一次校验,防止绕过初始检查。
- 在授权服务器侧也限制 client metadata 拉取目标。
步骤五:给工具调用加最小权限和审批
MCP 层适合做工具级审批,但不应该替代账号权限。每个工具应声明用途、参数风险和可执行范围。写文件、发消息、改数据、调用付费接口这类工具要单独列出,必要时要求人工确认。审批记录要包含工具名、参数、调用者、客户端和结果。
- 为每个工具定义允许的数据源和拒绝的操作。
- 高风险工具进入审批队列,不在客户端自动执行。
- 审批人能看到完整参数和影响范围,不能只看到“调用工具 A”。
- 执行后把审批记录、原始参数和返回结果写入审计日志。
步骤六:发布前测试拒绝路径和泄漏路径
生产测试至少要覆盖正常授权、拒绝授权、state 过期、redirect_uri 错误、SSRF 目标被拦截、工具审批拒绝和 token 过期。测试时还要检查响应里是否泄漏内部 URL、token 或堆栈信息。
- 用一个带内网地址的恶意 metadata URL 验证客户端会拒绝。
- 伪造 state 或重复使用旧 state,确认回调被拒绝。
- 测试 redirect_uri 前缀变体,确认不会匹配。
- 测试拒绝审批后工具没有副作用,并检查日志。
- 检查错误响应和 raw response 中不出现 token、cookie 或内网域名。
可复制上线模板
服务器名称:\ntransport:stdio / HTTP\n部署地址:\n客户端 client_id:\n允许 redirect_uri:\n第三方 API scope:\nper-client consent:已启用 / 未启用\nstate 生命周期:\nSSRF 拦截方式:\n高风险工具:\n审批渠道:\n审计日志位置:\n密钥轮换周期:\n最近一次上线测试:
实际例子:团队把内部 MCP 代理从本地实验切到正式环境
一个小团队先在桌面端跑了连接 Google Calendar 和内部工单的 MCP 服务器。正式上线前检查发现:所有成员共用同一个 client_id,redirect_uri 写成了前缀匹配,客户端还能访问 169.254.169.254。改造后为每个用户保存 client 授权记录,redirect_uri 改成完全匹配,出站流量走 egress proxy 并封掉 metadata 地址,写工单和发邮件工具接入审批。之后渗透测试中,伪造 state 和 SSRF 请求都被拒绝,日志也能定位到具体客户端和用户。
验收清单
- 生产入口使用 HTTPS,传输和监听范围已收窄。
- 每个客户端有独立 client_id,consent 是 per-client 记录。
- redirect_uri 完全匹配,state 单次使用且短过期。
- 客户端和授权服务器都有 SSRF 防护。
- 高风险工具已接入审批,并记录调用者与参数。
- 错误响应不泄漏 token、内网地址或堆栈。
- 拒绝、过期、伪造和越权路径都已测试。
常见坑
- 把 consent 做成“整个网站同意一次”,没有区分客户端。
- redirect_uri 用前缀匹配,攻击者可以注册类似路径。
- state 写入时机过早,让伪造授权请求可以绕过同意页。
- 只校验一次 IP,忽略 DNS rebinding 和重定向。
- 把 MCP 工具审批当成替代第三方服务权限。
- 日志里记录完整 token,导致审计本身变成泄漏源。
排错路径
- 回调一直失败:先检查 redirect_uri 是否与注册值完全一致,再看 state 是否已过期或重复使用。
- 授权后 token 无法访问第三方 API:核对 scope 是否完整,并检查第三方应用配置。
- 客户端请求被 SSRF 拦截:确认拦截逻辑覆盖重定向、DNS 解析和 IPv6。
- consent 页面没出现:检查 per-client 注册表和授权流程是否绕过代理。
- 工具审批不生效:确认高风险工具没有被另一个入口直接调用。
后续维护建议
更新日期:2026-08-16。MCP 安全最需要维护的是客户端注册表、redirect_uri 清单、scope 变化和工具审批人。建议每次新增服务器或第三方 API 时重新跑一遍本清单,每月轮换一次不需要长期保存的 token,每季度审查日志中的授权失败和 SSRF 拦截记录。协议版本升级后,还要重新对照官方安全文档确认旧配置没有被新的默认值覆盖。