A2A 推送接收方加固:SSRF、认证、重放与故障注入

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 端点对每个请求依次执行:

  1. 验证真实性:按约定的方案验签或验令牌(JWT 公钥、HMAC、API Key;配置里给了 token 就校验它);验证失败按 401 拒绝,不进入业务逻辑;
  2. 校验相关性:载荷里的任务 ID 必须是自己创建过的任务,否则丢弃——这挡住"合法签名但无关"的注入;
  3. 确认接收:验证通过即返回 2xx,把后续处理移出请求路径;
  4. 限流:按来源与任务维度限流,超限直接 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 推送通知安全 、流式传输与异步操作 。