A2A 推送接收方加固:SSRF、认证、重放与故障注入
推送通知把 A2A 服务器变成一个 HTTP 客户端、把您的服务变成一个对外的 Webhook 端点,两端各自承担一半安全责任。配置怎么建、通知怎么发,见流式传输与异步操作 ;规范的责任清单见推送通知安全 。这里给的是落地的实现顺序和能直接跑的故障注入。
威胁模型
| 攻击 | 入口 | 防在哪一端 |
|---|---|---|
| SSRF:借推送 URL 探测内网 | CreateTaskPushNotificationConfig 的 url、每次重定向 | 发送方(第 1 节) |
| 伪造通知:向 Webhook 注入假任务更新 | Webhook 端点 | 接收方(第 3 节) |
| 重放:重复投递同一通知破坏状态 | 至少一次的投递语义 | 接收方(第 4 节) |
| 洪泛:占满接收方或发送方队列 | 出站重试、入站请求 | 两端(第 4、5 节) |
1. 发送方:目的地址校验
SSRF 的关键不是"注册时查一次",而是每次真正建立连接之前都重新校验。DNS 记录可以在两次请求之间变化(DNS rebinding),重定向可以把公网地址换成内网地址。
收到 CreateTaskPushNotificationConfig(url)
├─ 语法层:必须是 HTTPS 地址,拒绝 userinfo、非标准端口(按策略)
├─ 解析 DNS → 得到 IP 列表
│ └─ 任一 IP 命中回环/私有/链路本地段 → 拒绝整个配置
├─ 存储配置(存 URL,不存当时的 IP)
└─ 投递时,对每次请求、每次重定向后:
├─ 重新解析 DNS → 重新校验全部 IP
├─ 用已校验的 IP 建立连接(不要重新解析)
└─ 校验失败 → 记日志、终止本次投递,不跟随该重定向私有段按规范译读列举:127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、localhost 与链路本地地址;有条件时叠加 URL 允许列表。应用层校验之外,在网络出口(egress 策略)再拦一层私有网段,两层独立生效。
2. 发送方:认证出站与有界重试
- 认证:按
TaskPushNotificationConfig.authentication指定的方案(scheme+credentials,如Bearer)为每个请求加凭据;凭据只用于出站方向,不写进日志; - 重试状态机:
投递 → 2xx:完成
→ 非 2xx 或超时(10–30 秒)→ 指数退避重试
→ 达到配置的连续失败上限 → 停止投递并记录- 停止投递不是错误:把该配置标记为不健康,等客户端重建配置或凭据轮换后再恢复;
- 任务终态通知与中间事件通知可以共用同一队列,但终态通知要保证最终至少发出一次——它承载"任务结束"这一不可丢的事实。
3. 接收方:验证每条通知
Webhook 端点对每个请求依次执行:
- 验证真实性:按约定的方案验签或验令牌(JWT 公钥、HMAC、API Key;配置里给了
token就校验它);验证失败按 401 拒绝,不进入业务逻辑; - 校验相关性:载荷里的任务 ID 必须是自己创建过的任务,否则丢弃——这挡住"合法签名但无关"的注入;
- 确认接收:验证通过即返回 2xx,把后续处理移出请求路径;
- 限流:按来源与任务维度限流,超限直接 2xx 丢弃或按策略拒绝,不让队列无界增长。
4. 接收方:幂等与重复投递
投递语义是至少一次,重复不可避免。幂等键用通知载荷里的任务 ID + 任务状态 + 事件序号(流式事件)或任务 ID + 终态(终态通知):
| 收到的通知 | 首次 | 重复 |
|---|---|---|
| 任务状态更新 | 推进本地状态机 | 校验与已存状态一致,静默确认 |
| 流式事件 | 追加到轨迹 | 按事件序号去重后确认 |
| 终态通知 | 触发收尾逻辑 | 只确认,不重复触发 |
重复处理逻辑的测试场景见生产就绪检查
第 3 节的 pushNotifications: true 行。
5. 故障注入
在测试环境对发送方与接收方分别注入下列故障;每项都应得到"安全失败",而不是异常或静默通过。脚本依赖 curl 与一个可控的接收端点:
#!/usr/bin/env bash
set -euo pipefail
: "${A2A_BASE_URL:?Set A2A_BASE_URL}"
: "${A2A_VALID_TOKEN:?Set A2A_VALID_TOKEN}"
: "${CALLBACK_BASE:?Set CALLBACK_BASE, e.g. https://callback.example.com/}"
: "${TEST_TASK_ID:?Set TEST_TASK_ID to a task created by this caller}"
create_config() { # task_id body_json → prints HTTP code (HTTP binding of CreateTaskPushNotificationConfig)
curl --silent --output /dev/null --write-out '%{http_code}' \
-H "Authorization: Bearer $A2A_VALID_TOKEN" -H 'Content-Type: application/json' \
-d "$2" "$A2A_BASE_URL/tasks/$1/pushNotificationConfigs"
}
expect() { # label expected_pattern actual
if [[ "$3" == $2 ]]; then printf 'OK %s -> %s\n' "$1" "$3"
else printf 'FAIL %s -> %s (expected %s)\n' "$1" "$3" "$2" >&2; exit 1; fi
}
# 1. Direct private address must never be accepted. Use a real test task ID; whether
# the server answers 400 (validation) or 404 (task scope) first is up to the
# implementation — the assertion is that it is NOT a 2xx and no config is created.
expect "private url rejected" 4?? "$(create_config "$TEST_TASK_ID" \
'{"url":"http://192.168.1.10/hook"}')"
# 2. Redirect into a private network must be rejected at delivery time:
# point the config at an endpoint you control that answers 302 -> http://127.0.0.1/x
# then trigger a task event and assert no request reached 127.0.0.1.
# 3. Replay: capture one delivered notification (headers + body), then:
replay_code="$(curl --silent --output /dev/null --write-out '%{http_code}' \
-H 'Content-Type: application/json' -H "Authorization: Bearer $VALID_CALLBACK_CREDENTIALS" \
--data-binary "$CAPTURED_BODY" "$CALLBACK_BASE/hook")"
expect "replayed notification accepted (idempotent)" 2?? "$replay_code"
# 4. Forged notification without credentials must be rejected before business logic.
expect "forged notification rejected" 401 "$(curl --silent --output /dev/null \
--write-out '%{http_code}' -H 'Content-Type: application/json' \
-d "$CAPTURED_BODY" "$CALLBACK_BASE/hook")"场景 2 需要一个可重定向的端点,故留为操作步骤:注册 https://<可控域名>/redirect(返回 302 Location: http://127.0.0.1/x),触发一次任务事件,然后断言 127.0.0.1 上没有收到请求、发送方日志记录了一次被终止的投递。
6. 观测
发送方关注:投递成功率、重试时长分布、放弃的配置数、被拒绝的不安全地址数(分类统计私网/回环/链路本地)。接收方关注:验证失败数(按原因分)、重复率、队列深度与限流触发数。两侧指标与生产就绪检查 第 4 节的日志字段对齐。
相关页面
版本与验证说明
对齐 A2A v1.0.1。pushNotificationConfig/set 的方法名与 TaskPushNotificationConfig 字段以数据结构
为准;脚本中的场景 1、3、4 在示例端点上验证过行为分类,场景 2 需要可控重定向端点,属于部署期检查。重试上限、退避参数是本站建议值,规范只约束量级(10–30 秒超时、指数退避)。
来源:A2A 推送通知安全 、流式传输与异步操作 。