Pydantic AI 本地 Web Chat 安全修复:Content-Type、Origin 与审批身份四层门禁
当 Agent 开始读文件、发邮件、跑 Shell 或调内部 API 时,一个只监听 127.0.0.1 的 Web UI 也不再是“只有本人能碰到”的界面。
核心判断只有一句:loopback 只是网络位置,不是信任边界;工具执行前必须同时通过请求、身份、审批和恢复四层门禁。

一、一个高危公告,暴露了两个错误默认
Pydantic AI 安全公告 GHSA-h4xc-3qfq-jf93 披露:Agent.to_web() 与 clai web 的开发 Web Chat 端点没有检查请求 Content-Type。开发者浏览不可信网页时,页面可向本机 Web Chat 发送不需要 CORS 预检的普通跨源请求,进而触发 Agent,并以本地进程的权限与凭据执行工具。
问题不只是“忘了一个 Header”,而是两个默认同时失效:
- 绑定
localhost就等于只有可信用户能访问; - 客户端回传的
approved=true可以当成服务端授权事实。
官方明确指出,requires_approval=True 也不能阻断这条链路,因为端点信任了客户端转发的审批结果。受影响范围为 pydantic-ai / pydantic-ai-slim 的 >=1.34.0,<1.107.4 与 >=2.0.0b1,<2.28.0;修复版为 1.107.4 和 2.28.0。
POST /chat HTTP/1.1
Host: 127.0.0.1:8000
Origin: https://untrusted.example
Content-Type: text/plain
这只是风险形状,不是利用步骤。服务端应在解析 Body、创建 Session 或运行 Agent 前就拒绝它。
二、修一个 Content-Type 还不够
Pydantic AI 修复后的聊天端点要求 Content-Type: application/json,并在解析 Body 和运行 Agent 前拒绝其他类型。这会挡住此次公告中的普通跨源请求链路,但不能取代完整的应用授权。
Content-Type不回答“谁在请求”;- CORS 主要约束浏览器脚本读取响应,不是通用身份系统;
- 即使入口正常,旧审批也可能在参数变化或进程重启后被误复用。
正确修复单位不是一个 Header,而是从 HTTP 入口到 Tool Executor 的整条授权链。
三、把工具执行拆成四层门禁
| 门禁 | 最小动作 | 过关证据 |
|---|---|---|
| 请求门禁 | 限定方法和 JSON,校验 Origin / Host,增加 CSRF 防护 |
非 JSON、未知 Origin 在 Agent 运行前返回 4xx |
| 身份门禁 | 服务端认证调用者,在创建 Session 前校验租户 / 资源 | 拒绝时无 Session、无模型调用、无输出 |
| 审批门禁 | 绑定主体、租户、Session / Run、工具、最终参数、资源和过期时间 | 工具名或有效参数变化后,旧审批必须失效 |
| 恢复门禁 | 恢复时查找同一 parked gate,校验 TTL、一次性与当前 Run | stale / 过期 / 已消费审批被拒绝,工具未执行 |
Mastra Core 1.58.0 的 resolveSession 可以在 Session、模型调用和输出前拒绝调用者;审批操作使用审批者自己的请求上下文重新解析 Session。找不到匹配 parked gate 时,onStaleToolApproval 可记录这次回答,但工具仍被拒绝。
四、用稳定绑定值拒绝“换参数继续跑”
from collections.abc import Mapping
from hashlib import sha256
import json
from typing import NewType
ToolName = NewType("ToolName", str)
ArgumentsDigest = NewType("ArgumentsDigest", str)
type JsonScalar = str | int | bool | None
def digest_arguments(
tool_name: ToolName,
arguments: Mapping[str, JsonScalar],
) -> ArgumentsDigest:
payload = json.dumps(
{"tool": tool_name, "arguments": dict(arguments)},
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return ArgumentsDigest(sha256(payload.encode()).hexdigest()[:16])
参数字段顺序变化不应制造新身份,但工具名、收件人、路径、命令或任何有效参数变化都必须产生新摘要。
def authorize(request, approval, attempt) -> bool:
request_allowed = all((
request.is_post,
request.is_json,
request.origin_allowed,
request.actor_authenticated,
request.tenant_authorized,
))
binding_matches = (
approval.actor_id == attempt.actor_id
and approval.tenant_id == attempt.tenant_id
and approval.session_id == attempt.session_id
and approval.run_id == attempt.run_id
and approval.tool_name == attempt.tool_name
and approval.arguments_digest == attempt.arguments_digest
)
recovery_allowed = (
attempt.parked_gate_id == approval.gate_id
and attempt.now_s < approval.expires_at_s
and not attempt.approval_consumed
)
return request_allowed and binding_matches and recovery_allowed
真实系统还要使用安全随机一次性值、持久化审批记录、原子消费与审计日志,不能把截断哈希当成唯一安全机制。
五、5 个失败样本先跑通
本地使用 Python 3.14.4 运行了 5 个场景:精确绑定被接受;非 JSON / 未知 Origin、主体错配、参数变化、无 parked gate / 已消费审批均被拒绝。
..... [100%]
5 passed in 0.01s
py_compile 和代码规则审计同时通过。完整自检已保存在本地知识库证据目录;本文代码块摘录核心逻辑。
六、按四阶段收紧
- **先停止暴露面:**升级到
1.107.4/2.28.0或更高;无法升级时,停止在浏览不可信页面期间运行 Web Chat,且不暴露有副作用工具。 - **在 Agent 运行前拦请求:**非 JSON、未知 Origin 和未认证请求必须在 Body 解析、Session 创建和模型调用前失败。
- **重新绑定审批:**审批者身份、租户、Run、工具与最终参数一起固定;只改一个有效参数,旧审批就失效。
- **为恢复建失败路径:**重启后找不到原 parked gate,或审批过期 / 已消费时,记录原因并拒绝执行。
七、上线前检查表
- Pydantic AI 运行时版本已脱离受影响范围;
- 非 POST、非 JSON 与未知 Origin 在 Agent 运行前被拒绝;
- 认证失败时没有创建 Session、模型调用或频道输出;
- 审批绑定调用者、租户、Session / Run、工具、最终参数与资源;
- 工具名或有效参数变化后,旧审批无法继续执行;
- 过期、已消费或无 parked gate 的审批统一拒绝;
- 失败样本验证的是“工具未执行”,不是只看前端提示。
官方来源:Pydantic AI 安全公告、Pydantic AI v2.28.0、Mastra Core 1.58.0。
验证边界:本文基于 2026-08-13 对官方安全公告与 Release 的核验,并运行了框架无关的 Python 标准库自检;未安装或升级 Pydantic AI / Mastra,未构造攻击页面,未启动真实 Agent Web Chat,也未执行有副作用工具。
更多推荐

所有评论(0)