当 Agent 开始读文件、发邮件、跑 Shell 或调内部 API 时,一个只监听 127.0.0.1 的 Web UI 也不再是“只有本人能碰到”的界面。

核心判断只有一句:loopback 只是网络位置,不是信任边界;工具执行前必须同时通过请求、身份、审批和恢复四层门禁。

本地 Agent Web UI 四层门禁

一、一个高危公告,暴露了两个错误默认

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.42.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.0resolveSession 可以在 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. **先停止暴露面:**升级到 1.107.4 / 2.28.0 或更高;无法升级时,停止在浏览不可信页面期间运行 Web Chat,且不暴露有副作用工具。
  2. **在 Agent 运行前拦请求:**非 JSON、未知 Origin 和未认证请求必须在 Body 解析、Session 创建和模型调用前失败。
  3. **重新绑定审批:**审批者身份、租户、Run、工具与最终参数一起固定;只改一个有效参数,旧审批就失效。
  4. **为恢复建失败路径:**重启后找不到原 parked gate,或审批过期 / 已消费时,记录原因并拒绝执行。

七、上线前检查表

  • Pydantic AI 运行时版本已脱离受影响范围;
  • 非 POST、非 JSON 与未知 Origin 在 Agent 运行前被拒绝;
  • 认证失败时没有创建 Session、模型调用或频道输出;
  • 审批绑定调用者、租户、Session / Run、工具、最终参数与资源;
  • 工具名或有效参数变化后,旧审批无法继续执行;
  • 过期、已消费或无 parked gate 的审批统一拒绝;
  • 失败样本验证的是“工具未执行”,不是只看前端提示。

官方来源:Pydantic AI 安全公告Pydantic AI v2.28.0Mastra Core 1.58.0

验证边界:本文基于 2026-08-13 对官方安全公告与 Release 的核验,并运行了框架无关的 Python 标准库自检;未安装或升级 Pydantic AI / Mastra,未构造攻击页面,未启动真实 Agent Web Chat,也未执行有副作用工具。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐