返回 AICAP-180
B6 · Day 58真 MCP server (2026-07-28 spec)

工具输入零信任校验

Day 57 我们让「自己的」LLM 当客户端去调工具——很容易产生「客户端是我的,输入可信」的错觉。今天专门打破它:即便客户端是自己的模型,server 也必须零信任校验每一条入参。这是 B6 从「让它跑通」转向「让它扛得住」的安全转折,也是 B7(OAuth 2.1 + MCP 安全 + CI gate)的前置防线。承接 Day 53 写好的 Zod/JSON Schema(那时为「类型正确」

阶段: B6 · 真 MCP server (2026-07-28 spec)(Day 51-60) 标签: #zero-trust #schema-validation #mcptox #json-rpc

今日导引(由浅入深)

Day 57 我们让「自己的」LLM 当客户端去调工具——很容易产生「客户端是我的,输入可信」的错觉。今天专门打破它:即便客户端是自己的模型,server 也必须零信任校验每一条入参。这是 B6 从「让它跑通」转向「让它扛得住」的安全转折,也是 B7(OAuth 2.1 + MCP 安全 + CI gate)的前置防线。承接 Day 53 写好的 Zod/JSON Schema(那时为「类型正确」,今天为「对抗恶意」复用同一份 schema);明天(Day 59)验证迁移上网没引入行为漂移。今日「最小可判定产出」:9 条恶意输入被 -32602 全部拦截(目标 9/9)。

1. 机理精读

零信任的核心命题:信任边界不在「谁是客户端」,而在「server 收到的每个字节」。即便上游是你自己部署的 DeepSeek 客户端,模型也可能(被投毒 / 幻觉 / bug)填出越界值、缺必填字段、错误类型的入参。MCP server 处在网络边界上,必须把每条 tools/callarguments 当不可信输入对待——这就是 schema-first 校验:声明 schema,运行时强制,校验失败前不让任何业务逻辑执行

schema 校验是「最小防线」,不是「全部防线」。它能挡三类结构性攻击:(1) 越界值(amount 超出 minimum/maximum、超出 enum);(2) 缺必填required 字段缺失);(3) 错误类型(该传 string 传了 number/array)。这三类正是 Day 58 实战要注入的 3 类恶意输入。校验失败时,server 必须返回 JSON-RPC 标准错误 -32602 INVALID_PARAMS,而不是把脏数据吞进 handler。

schema 挡得住结构,挡不住语义。这是今天必须讲清的边界:tool poisoning(工具描述投毒,MCPTox 2026)是 schema 完全无能为力的独立风险。投毒攻击不违反任何 schema——它通过合法的 description 字段植入恶意指令,诱导 LLM 客户端误调工具或泄露上下文。schema 校验的是「入参形状」,投毒攻击的是「模型决策」,两者在不同层。所以 SOTA 检查的硬告警是:仅靠 schema 就声称安全是错的——description 投毒、confused-deputy(混淆代理)等需配 OAuth + 工具来源审查(B7 处理),今天只覆盖输入层。

把 MCP 的攻击面分层,能看清今天覆盖到哪一层

攻击形态防御手段谁覆盖
L1 入参结构越界/缺字段/错类型schema 校验(-32602本日 Day 58
L2 工具描述tool poisoning(投毒诱导误调)工具来源审查 + 描述审计B7
L3 授权边界confused-deputy(混淆代理越权)OAuth 2.1(RFC 9728/8707)B7
L4 传输中间人/重放TLS + 资源指示符B7

今天只把 L1 钉死——这是「最小防线」三个字的精确含义:必要、但远不充分。把这四层都误以为「schema 能搞定」,就是最危险的安全错觉。

为什么把校验做在 server 而不只在 client? 因为 client 可被绕过、可被替换、可有 bug。「在客户端校验过了」不能成为「server 信任输入」的理由——这是纵深防御的基本原则。toolRegistry.ts 的设计正体现这点:call()(第 183 行)在进 handler 前强制 validate(),没有「跳过校验」的快路径。

零信任在 AML 域为何尤其关键? 因为入参直接影响合规判定与 SAR 草稿内容:

  1. caseId 是脏值(不存在的案件 / 注入字符串),handler 取数会失败或取错案,产出错误的可疑活动结论。
  2. 若数值参数越界(如负的金额、超大窗口天数),可能让规则引擎产出无意义的得分。
  3. 若类型错误(数组冒充字符串),handler 内部可能抛未捕获异常,泄露堆栈/内部结构给调用方。

schema 校验把这三类挡在 handler 之前,等于给合规逻辑加了一道「只接受形状合法的证据」的门——这与 auditTrail.ts 的「只记录可验证的事件」是同一种合规纪律。

2. 推导 / 手算 / 代码走读

Day 58 复用 Day 53 的 Zod shape / toolRegistry.tsvalidate() 做校验。逐条走读(Read 验证自 src/agent/mcp/toolRegistry.ts):

  • validate(schema, value, path='$'): string[](第 112 行)——返回错误信息数组,空数组 = 通过。它是零信任的执行点。
  • 类型校验:第 114 行 if (!typeOk(value, schema.type)) 先验类型,不符立即 push 错误并 return(第 116 行,type 不符则不再深入)。typeOk(第 95 行)逐类型判定,注意 number 要求 Number.isFinite(第 100 行,挡 NaN/Infinity),integer 要求 Number.isInteger(第 102 行)。→ 这挡「错误类型」恶意输入。
  • 枚举校验:第 118 行 if (schema.enum && !schema.enum.includes(...)) push「not in enum」错误。→ 挡部分「越界值」。
  • 数值边界:第 126-129 行对 number/integer 校验 minimum/maximum,越界 push「< minimum」/「> maximum」。→ 挡「越界值」恶意输入。
  • 必填校验:第 135 行 for (const req of schema.required ?? []) 缺字段 push「required property missing」(第 136 行)。→ 挡「缺必填」恶意输入。
  • 递归:数组按 items 递归(第 130-132 行),对象按 properties 递归(第 133-141 行),路径 path 一路拼接便于定位。
  • 错误码绑定call()(第 183 行)拿到 validate 的非空 errs 后,throw new McpCallError(RPC.INVALID_PARAMS, ...)(第 187 行)。RPC.INVALID_PARAMS = -32602(第 86 行常量)。
  • JSON-RPC 信封映射handle()(第 199 行)捕获 McpCallError,折成 response.error(第 211-213 行 { code: e.code, message, data: e.data }),永不抛错(第 197 行注释「所有失败折成 response.error」)。所以网络侧拿到的就是合法的 -32602 错误响应,而非 500。

结论:9 条恶意输入(3 工具 × 3 类)经 tools/call → handle → call → validate 路径,结构性违规全部被 -32602 拦截在 handler 之前——这是今天「9/9」的代码依据。

一条恶意输入的完整拦截轨迹(以 assessCase 缺必填 caseId 为例,逐行对应代码):

// 请求:缺 caseId(恶意「缺必填」)
{ "jsonrpc":"2.0","id":3,"method":"tools/call",
  "params":{ "name":"assessCase","arguments":{} } }
  1. handle()(第 199 行)进入 tools/call 分支,name="assessCase" 非空,进 try(第 207 行)。
  2. call("assessCase", {})(第 183 行)查到工具,调 validate(inputSchema, {})(第 186 行)。
  3. validate 第 135 行 for (const req of schema.required) 遍历到 caseId,第 136 行 if (!('caseId' in {})) 成立 → push "$.caseId: required property missing"
  4. errs.length > 0 → 第 187 行 throw new McpCallError(RPC.INVALID_PARAMS, ...)RPC.INVALID_PARAMS = -32602
  5. handle() 第 211 行 catch (e instanceof McpCallError) → 折成 error(第 212 行)。
// 响应:被 -32602 拦截,handler 从未执行
{ "jsonrpc":"2.0","id":3,
  "error":{ "code":-32602,"message":"invalid params for assessCase",
            "data":["$.caseId: required property missing"] } }

「错误类型」「越界值」两类恶意输入走的是 validate 的第 114 行(typeOk)/ 第 126-129 行(minimum/maximum),终点同样是 -32602。9 条恶意输入对应这 3 类各 3 条,全部在 handler 之前被截停。

3. 今日实战

  1. 对 3 个 AML 工具(assessCase/draftSar/listTypologies)各构造 3 类恶意输入,共 9 条:
    • 越界值:如 assessCase 的数值参数填超出 maximum 的值。
    • 缺必填:删掉 required 字段(如 assessCasecaseId)。
    • 错误类型:该传 string 的填 number/array。
  2. 把 9 条各封进 {jsonrpc:'2.0',id,method:'tools/call',params:{name,arguments}},经 server 的 handle()(或 HTTP tools/call)打进去。
  3. 断言每条返回 error.code === -32602,统计拦截数。
  4. 校验逻辑复用 Day 53 的 Zod shape(或 toolRegistry.tsvalidate() 对照基线),不另写校验。
  5. 接入测试套件,作为零信任回归用例。

4. 今日实测 / 产出

  • 状态:待建——数字目标:9/9 恶意输入被 -32602 拦截
  • 纯校验路径,本地可验,无需 key

(按诚信纪律:「待建」逐字保留;「9/9」是目标,未臆造为已测得。)

5. 常见误区 / 陷阱

  • 仅靠 schema 就声称安全:SOTA 检查硬告警。schema 挡入参结构,挡不住 description 投毒、confused-deputy——那需 OAuth + 工具来源审查(B7)。
  • 把业务/校验错误塞进 200 的 result:校验类错误必须走 JSON-RPC error 通道(-32602)才能被客户端正确处理(呼应 Day 55 SOTA)。塞进 result 会让客户端误以为调用成功。
  • 「客户端是自己的所以输入可信」:纵深防御的反面教材。client 可被绕过/替换/有 bug,server 必须独立校验。
  • 只测 happy-path 不测恶意输入:零信任校验的价值正在「拒绝得对」,不测拒绝路径等于没测安全。
  • 校验逻辑双写漂移:若 client 一份校验、server 另一份校验,两份易漂移;以 Day 53 的 Zod shape 作单一真相源,server 端复用,避免「client 放过、server 拦截」的不一致。
  • 把 NaN/Infinity 当合法 number 放过typeOk(第 100 行)特意要求 Number.isFinite——若自己写校验忘了这点,amount=Infinity 会绕过 maximum 检查(任何上限都 < Infinity 永真)。

6. 学习资源(每条带 YYYY-MM)

  • MCPTox benchmark(2026)—— tool poisoning / description 投毒攻击基准;schema 无法防御的语义层风险。
  • MCP 规范(2026-07-28,硬复查点 07-28)—— tools/call 入参校验与 -32602 错误语义。
  • JSON-RPC 2.0 规范(2010-03,长期稳定)—— -32602 INVALID_PARAMS 标准错误码定义。
  • Zod v3 文档(2026,v4 已 RC)—— schema-first 运行时校验;升级前查 z.toJSONSchema 行为变更(呼应 Day 53)。
  • NIST CAISI AI Agent 标准(2026-02 启动)—— AI agent 工具调用安全基线方向。

SOTA检查 (2026-06 更新)

  • 当前主流方案:schema-first 校验是 MCP 安全基线共识,2026-06 仍是入门级标配,无替代。
  • 是否仍 SOTA:作为「输入层最小防线」是,但单靠它不构成安全
  • 过时黑名单 / AVOID:禁止仅靠 schema 就声称安全——description 投毒、confused-deputy 等需配 OAuth + 工具来源审查(B7 处理),本日只覆盖输入层。禁止把校验错误塞进 200 result,必须走 -32602 error 通道。
  • 下次复查点:B7 接入 OAuth 2.1(RFC 9728/8707)后,复审「输入层 + 鉴权层」是否覆盖 confused-deputy;07-28 MCP 规范定稿后重验错误码字段。

衔接

  • 昨天:Day 57 — DeepSeek-V3 作 MCP 客户端(暴露了「客户端可自由填参」的攻击面)。
  • 今天:server 零信任校验——9 条恶意输入经 validate()-32602 拦截(目标 9/9),并讲清 schema 挡不住语义投毒的边界。
  • 明天:Day 59 — 进程内 mock vs 真 server 等价回归,用对拍验证迁移上网无行为漂移。