A2A Push:直连IP检查与代理后目标检查不是一回事
A2A Push:直连IP检查与代理后目标检查不是一回事
知识点轴:A2A / sandbox-security / harness-runtime / MCP Gateway / benchmark-evaluation。 依据 Go v2.4.0 固定sender源码、Go release、Python v1.2.2 sender源码 与相关 PR;2026-07/09发布的旧修复于本日交叉核验,不是今日新漏洞或用户现网故障。已读源码和上游测试相关段,未安装SDK/运行回调E2E;原文选段见 raw/articles/a2a-push-go-python-fixed-source-selected-2026-10-08.md。
为什么值得用户深挖
A2A 的 Agent Card、任务owner以及push回调是不同安全层。一个被授权设置回调的调用者可以令agent服务器发出HTTP POST;这条服务器出站不由Card签名或任务状态CAS保护。Hermes/企业agent若未来把回调放在网关/本地代理后,单纯说“默认阻止私网IP”不能说明最终 webhook 目的地已拒绝私网。该出站权限归属 A2A-Agent2Agent-Protocol 与 MCP-Gateway-Runtime 的跨协议边界;contextforge-egress-path-parity-dns-rebinding 已提出连接时IP和逐路径核验,本例补充直连与代理下的两级目的地址,并将投递失败的可观察性纳入验收。
固定版本的机制对照
| 层 | Go SDK v2.4.0(发布07-28) | Python SDK v1.2.2(10-03 changelog,GitHub于10-05展示) |
| 注册/出站拦截 | NewHTTPPushSender(nil) 默认启用 guarded transport;net.Dialer.Control 在TCP握手前检查所拨IP,拦loopback、private、link-local、unspecified、multicast。AllowPrivateNetworks=true 可关闭该guard | sender的 push_url_validator 可注入,默认None,即sender本身不作库内URL筛查;已合并 #1173 登记时和 #1164 派发时验证路径,实际调用方是否注入validator及有效配置要另核 |
| DNS/代理 | 直连每次拨号时核IP,覆盖新建连接的redirect目标;transport同时设 ProxyFromEnvironment。若代理被选中,拨号IP属于代理,不必是原始webhook终点;不能把“检查代理IP”写成“检查代理出站目的IP” | #1164 明述验证时DNS与POST拨号时解析是两次动作,残余rebinding;httpx客户端由调用方注入,不能假定代理/redirect均受validator覆盖 |
| 凭据/redirect | 最多10跳;跨req.URL.Host会删除自定义A2A-Notification-Token,Go标准客户端对跨host Authorization 的处理另依赖库行为;同host不同路由/最终代理仍需检验 | 固定sender在有token/auth配置时随请求发送;凭据是否到正确接收方、接收方是否验权需独立测 |
| 失败可见性 | FailOnError=false 默认:被guard挡住或非2xx,记日志后SendPush向调用方返回nil;不能只以nil断言“已投递”。FailOnError=true才将错误上抛且可能取消执行 | sender _dispatch_notification 对validator拒绝返回False,send_notification只汇总警告;调用方要单独核回调接收侧及任务进展 |
Go旧 #373 的初始代码问题已由#374合并并包含于v2.4.0;不能把旧issue全文当今天仍未修。Python旧 #786 的“无任务owner”段也被维护者指出自v1.0已有owner模型;必须按时间线看评论与固定tag,既不能以Card证明task隔离,也不能沿用过时IDOR断言。上述配置差异不代表一个SDK整体比另一个安全。^[raw/articles/a2a-push-go-python-fixed-source-selected-2026-10-08.md]
一阶原理:谁持有最终出站权
task owner 授权 → 配置/更新回调URL → sender选代理与重定向 → socket dial 到直连目标或代理 → 代理二次连接 → receiver接收/验权。同一个输入URL可以在多个时点变成不同IP;直连Dialer.Control在连接IP处拒绝是强于URL字符串校验的机制,但代理模式下应用侧只能见自己拨的代理地址,代理上游最终访问地址必须在代理出口/egress policy 再作可见且强制的判定。若公司内网代理本身位于私网,Go默认guard有可能拒绝合法代理连接而不是准确判断webhook;若代理为公网,应用侧guard未必覆盖代理能访问的私网目标。这里是代码机制推出的待隔离验证的失效模式,不是已复现的远端穿透。
重定向另要核每跳URL、DNS、IP、token/audience;Go跨host去token的实现比只限跳数更细,但req.URL.Host相同≠所有副作用或凭据都在同一权限域。Go sender不会因源代码里存在guard自动证明所有生产HTTP client/手工sender/sidecar都使用它。a2a-python-cluster-state-stream-execution-boundaries 的共享状态/事件和caller owner是另一条账,不是webhook出口的授权证明。
Hermes/llm-wiki可执行验收(建议,未对用户环境执行)
- 隔离mock webhook与显式代理,逐项记
SDK tag / sender构造 / HTTP(S)_PROXY与NO_PROXY有效值 / registrar和caller / 原URL / 每跳URL / dial IP / 代理最终target IP / token去向 / 接收端任务ID与签名/认证 / sender返回与日志 / 实际投递。测试仅用私有实验网络,不访问真实metadata、用户生产系统。 - 配对跑直连公网允许、直连回环拒绝、私网代理承载合法外部回调、公网代理被请求访问mock私网地址、307跨host/同host重定向、DNS重新解析、validator禁用/opt-out;每组同时核授权合法成功和拒绝后零网络effect,不能凭错误返回代替网络层回读。Go的
FailOnError=false场景单列“未投递但调用返回nil”。 - 根据结果把eager URL筛查、直连时dial-IP guard、代理出口策略和receiver验权分别归到责任层;选择允许内部webhook时,应显式授权host/CIDR/owner而非在无人值守环境无条件放开
AllowPrivateNetworks。扩展到 MCP Gateway/REST/A2A不同handler时仍逐路径测,保留运行版本、配置与分母。
边界与待证
只读取Go固定源码206行、测试相关片段、release和Python固定sender/两个PR;没有启动Go/Python sender或代理,没有做本地安全复现,不知道用户是否部署这些SDK。proxy分支的具体穿透能力取决于代理实现、proxy auth、NO_PROXY、目标DNS与出站ACL,当前为设计风险假设;是否已有公司出口防护未知。上游Go测试证明作者写了若干正反例而不是本轮实跑通过。后续应先在隔离环境取证再升级为漏洞判断。
写入记录
- 2026-10-08 12:00 CST:补正文A2A/MCP Gateway主题簇入口,避免仅依赖发布时剥离的frontmatter related。
- 2026-10-08 09:03 CST:交叉读取Go/Python固定版本与旧issue修复时间线;拆分直连/代理/receiver三层安全责任及返回nil与实际投递分母,提出不触生产的配对验证。