a2aproject / a2aproject/a2a-js

[Bug]: push-notification webhook URL is not SSRF-validated (A2A spec §13.2 unimplemented)

オープン
#584 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
TypeScript
スター
613
フォーク
169
平均マージ
1日 6時間
マージ済み PR(30日)
21

説明

### What happened?

### Summary

#350 asks for SSRF and DNS-rebinding protection on the **client** side (`ClientFactory`, agent-card
discovery, `DefaultAgentCardResolver`). This issue is the **server-side half of the same problem**, which
#350 doesn't cover: the SDK's push-notification sender POSTs to a client-supplied webhook URL with no
validation at all.

A2A spec §13.2 puts this on the agent:

> Agents **SHOULD** validate webhook URLs to prevent SSRF (Server-Side Request Forgery) attacks: Reject
> private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16); Reject localhost and
> link-local addresses; Implement URL allowlists where appropriate.

Verified at `cfeed0d` (v0.3.14). The same gap is tracked for the Python SDK in a2a-python#786.

### Where

No validation exists on any hop of the server-side path:

- `src/server/request_handler/default_request_handler.ts:229` (`sendMessage`) and `:331`
(`sendMessageStream`) — save an inline `configuration.pushNotificationConfig`, gated **only** on
`agentCard.capabilities.pushNotifications`. `:449` (`setTaskPushNotificationConfig`) — same, no check.
- `src/server/push_notification/push_notification_store.ts` — `save()` stores the URL as-is.
- `src/server/push_notification/default_push_notification_sender.ts` — `_dispatchNotification`:
`const url = pushConfig.url; await fetch(url, { method: 'POST', ... })`.

### Attack scenario

- **Victim:** an operator running an agent built on `@a2a-js/sdk` with push notifications enabled
(`capabilities.pushNotifications: true` + the SDK's `DefaultPushNotificationSender`), deployed
somewhere with internal-only services (admin APIs, sidecars, cloud metadata).
- **Attacker:** any client the agent will serve. No elevated privilege required — and because the config
can be supplied inline on `message/send`, **no pre-existing task is required either**; the attacker's
first request is enough.
- **Result:** the agent POSTs the task payload to an attacker-chosen internal address, from inside the
victim's network, carrying the attacker's `X-A2A-Notification-Token`.

### Reproduction

Wire an agent with the SDK's own components (`DefaultRequestHandler` + `InMemoryTaskStore` +
`InMemoryPushNotificationStore` + `DefaultPushNotificationSender`) and an agent card declaring
`capabilities: { pushNotifications: true }` — i.e. the wiring in your own
`test/server/push_notification_integration.spec.ts`. Start a listener on a loopback address to stand in
for an internal-only service, then send one message pointing the webhook at it:

```ts
await handler.sendMessage({
message: { messageId: 'm1', role: 'user', parts: [{ kind: 'text', text: 'hi' }], kind: 'message' },
configuration: {
pushNotificationConfig: {
id: 'cfg-1',
url: 'http://127.0.0.1:/internal-admin/keys', // §13.2 says reject 127.0.0.0/8
token: 'attacker-chosen-token',
},
},
});
```

**Observed:** the loopback listener receives `POST /internal-admin/keys` with the task payload and
`x-a2a-notification-token: attacker-chosen-token`. **Expected per §13.2:** the URL is rejected.

The same happens via `sendMessageStream`. And `setTaskPushNotificationConfig` accepts every address class
the spec says to reject — no error for any of these:

```ts
'http://169.254.169.254/latest/meta-data/' // link-local / cloud metadata
'http://10.0.0.1/admin' // private
'http://192.168.1.1/admin' // private
'http://172.16.0.1/admin' // private
'http://localhost:9999/x' // localhost by name
'file:///etc/passwd' // scheme unrestricted
```

### Impact

Any client of a push-enabled agent can make it issue attacker-directed POSTs from inside its trust
boundary: reaching internal-only services (including loopback/RFC-1918), cloud metadata
(`169.254.169.254`, environment-dependent), enumerating internal hosts/ports via timing/error
differences, and delivering task state to an attacker-chosen endpoint. Since the vulnerable sender and
store are the SDK's **default** components, every push-enabled agent built on `@a2a-js/sdk` inherits it.

### Note

`fetch()` is called with no `redirect` option, so it defaults to `follow`. A webhook URL that passes a
naive string check can `307`-redirect to an internal address and the SDK follows it, POST and body
preserved. So validating the URL string alone isn't enough — the resolved IP needs to be validated and
pinned. (This is the same DNS-rebinding concern #350 raises for the client side.)

### Proposed direction (looking for input from reviewers)

The intent is **not** to block private/loopback unconditionally — those are legitimate targets for
internal agents, sidecars, and dev setups. (a2a-python#895 was closed for validating unconditionally; the
maintainers' follow-up design is a2a-python#1023.) Mirroring that:

1. A **composable, opt-out** validator: reject private/loopback/link-local by default, with a host/CIDR
allowlist.
2. Restrict scheme to HTTP(S) (or HTTPS-only, configurable).
3. **Validate, then connect to the pinned resolved IP**; set `redirect: 'error'` on the dispatch `fetch`.
4. Make it injectable so integrators can supply their own policy.

Since #350 wants the client-side half, one shared validator could serve both — which is what a2a-python#1023
scopes (`UrlValidator` + per-domain wrappers). Happy to implement it here if that direction looks right.

---

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

The issue points to specific files: src/server/request_handler/default_request_handler.ts, src/server/push_notification/push_notification_store.ts, and src/server/push_notification/default_push_notification_sender.ts. Start by examining the _dispatchNotification method where fetch is called. Understand the existing push notification flow and the proposed validation design from linked Python issues. Implementing SSRF validation requires knowledge of URL parsing, IP address validation (private ranges, localhost, link-local), and handling DNS rebinding. Check test/server/push_notification_integration.spec.ts for existing integration tests. Done looks like a validator that rejects specified URL patterns, restricts schemes, and pins resolved IPs, with configurable allowlists.

索引モデルが issue の本文から書いたものです。

評価

技術スタック
node.js, typescript
領域
backend-api-design, security
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
静か
明瞭さ
明確に書かれている
初心者へのやさしさ
45/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。