A2A Push Receiver Hardening: SSRF, Authentication, Replay, and Fault Injection
Push notifications turn an A2A server into an HTTP client and your service into a public webhook endpoint; each side carries half the security responsibility. How configs are created and notifications delivered is covered in Streaming and asynchronous operations ; the normative duty list is Push notification security . This page provides the implementation order and fault injections you can run directly.
Threat model
| Attack | Entry point | Defended on |
|---|---|---|
| SSRF: probing the internal network through the push URL | CreateTaskPushNotificationConfig’s url, every redirect | Sender (section 1) |
| Forged notifications: injecting fake task updates into the webhook | The webhook endpoint | Receiver (section 3) |
| Replay: duplicate deliveries corrupting state | The at-least-once delivery semantics | Receiver (section 4) |
| Flooding: saturating either side’s queues | Outbound retries, inbound requests | Both (sections 4, 5) |
1. Sender: validating the destination
The key to SSRF is not “check once at registration” but revalidating before every connection is actually established. DNS records can change between requests (DNS rebinding), and a redirect can swap a public address for an internal one.
CreateTaskPushNotificationConfig(url) received
├─ Syntax: must be an HTTPS URL; reject userinfo and non-standard ports (per policy)
├─ Resolve DNS -> list of IPs
│ └─ any IP in loopback/private/link-local ranges -> reject the whole config
├─ Store the config (the URL, not the IPs seen at registration)
└─ At delivery, for every request and after every redirect:
├─ resolve DNS again -> re-validate every IP
├─ connect using the validated IP (do not resolve again)
└─ validation failure -> log, abort this delivery, do not follow the redirectPrivate ranges follow the translated specification: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, localhost, and link-local addresses; add a URL allowlist where feasible. Beyond application checks, enforce a second layer at the network egress policy so the two layers fail independently.
2. Sender: authenticated outbound and bounded retries
- Authentication: attach credentials per
TaskPushNotificationConfig.authentication(scheme+credentials, e.g.Bearer) to every request; the credentials are outbound-only and never logged; - Retry state machine:
deliver -> 2xx: done
-> non-2xx or timeout (10-30 s) -> retry with exponential backoff
-> consecutive-failure cap reached -> stop delivering and record- Stopping is not an error: mark the config unhealthy and resume only after the client recreates the config or credentials rotate;
- Terminal-state notifications and intermediate events may share a queue, but terminal notifications must be delivered at least once eventually — they carry the non-droppable fact that a task has ended.
3. Receiver: verifying every notification
The webhook endpoint applies, in order, to every request:
- Verify authenticity: validate the signature or token per the agreed scheme (JWT public key, HMAC, API key; check the config’s
tokenwhen provided); a failed check rejects with 401 before any business logic; - Check relevance: the task ID in the payload must be a task you created, otherwise discard — this stops “validly signed but unrelated” injections;
- Acknowledge: return 2xx once verified, moving further processing off the request path;
- Rate-limit: by source and by task; drop (or reject per policy) rather than let queues grow unbounded.
4. Receiver: idempotency and duplicates
Delivery is at-least-once; duplicates are guaranteed. The idempotency key is the notification’s task ID + task state + event sequence (streamed events) or task ID + terminal state (terminal notifications):
| Notification received | First time | Duplicate |
|---|---|---|
| Task status update | Advance the local state machine | Verify consistency with stored state, acknowledge silently |
| Streamed event | Append to the trace | Deduplicate by event sequence, then acknowledge |
| Terminal notification | Trigger finalization | Acknowledge only; do not re-trigger |
Test scenarios for duplicate handling are the pushNotifications: true row in section 3 of the production readiness guide
.
5. Fault injection
Inject the following faults at sender and receiver in a test environment; each must fail safely rather than crash or silently pass. The script depends on curl and a controllable receiver endpoint:
#!/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")"Scenario 2 needs a redirectable endpoint, so it stays an operational step: register https://<your-domain>/redirect (serving 302 Location: http://127.0.0.1/x), trigger one task event, then assert that nothing arrived on 127.0.0.1 and the sender logged an aborted delivery.
6. Observability
The sender tracks delivery success rate, retry-latency distribution, abandoned config count, and rejected unsafe addresses (split by private/loopback/link-local). The receiver tracks verification failures (by reason), duplicate rate, queue depth, and rate-limit hits. Both align with the logging conventions in section 4 of the production readiness guide .
Related pages
- Streaming and asynchronous operations : how push notifications work, with authentication schemes
- Push notification security : the normative duty list and private ranges
- Task idempotency and recovery : idempotency patterns on the state side
- Security implementation : the main authentication and authorization line
Version and verification notes
Aligned with A2A v1.0.1. Method names and the TaskPushNotificationConfig fields follow the data structures
; scenarios 1, 3, and 4 were verified against an example endpoint for behavior classes, while scenario 2 requires a controllable redirect endpoint and belongs to deployment-time checks. Retry caps and backoff parameters are this site’s recommendations; the specification constrains only the magnitude (10-30 second timeouts, exponential backoff).
Sources: A2A Push notification security , Streaming and asynchronous operations .