~ / search / GHSA-q78c-5jjq-57g8

GHSA-q78c-5jjq-57g8

A2A Java SDK has Server-Side Request Forgery (SSRF) in push-notification delivery

Source: GitHub Advisory Database (reviewed) Moderate CVE-2026-111761

Packages

EcosystemPackageAffectedFixed in
Maven org.a2aproject.sdk:a2a-java-sdk-server-common < 1.3.0.Final 1.3.0.Final

Details

As written in the advisory (Markdown source, shown as plain text).

--- - **Ecosystem:** Maven - **Affected package:** `org.a2aproject.sdk:a2a-java-sdk-server-common` (push-notification delivery) - **Affected versions:** `<= 1.2.0.Final` (and `1.2.1.Final-SNAPSHOT` on `main` prior to the fix) - **Patched version:** _(the release that carries the fix)_ - **CWE:** CWE-918 (Server-Side Request Forgery) - **Severity:** High - **CVSS v3.1:** `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N` (5.8, Moderate) --- ## Summary An **unauthenticated** attacker can coerce the a2a-java server into issuing HTTP requests to arbitrary internal and cloud-metadata endpoints from the server's own network position — a Server-Side Request Forgery reachable in the default deployment with no credentials, no user interaction, and a single request. The behaviour is the shipped default, not a misconfiguration. ## Details `BasePushNotificationSender` (in `server-common`) POSTs task data to the URL stored on a task's `TaskPushNotificationConfig` with no validation of the destination: no block on loopback, RFC1918, link-local (`169.254.0.0/16`, including the `169.254.169.254` metadata address), unique-local, unspecified, or multicast ranges; no scheme restriction; and no bound on redirects. `InMemoryPushNotificationConfigStore.setInfo` stores the URL verbatim and `onCreateTaskPushNotificationConfig` does not check it either. Both shipped HTTP clients follow redirects (`JdkA2AHttpClient` → `followRedirects(NORMAL)`, `VertxA2AHttpClient` → `setFollowRedirects(true)`), so a creation-time URL allowlist is bypassable: an attacker webhook answers `302 Location: http://169.254.169.254/...` and the client follows it. A hostname check is likewise defeated by DNS rebinding. A correct guard must validate the *resolved* IP of every hop at connection time, not the URL string. ## Exploitation Demonstrated end-to-end against a running server (default wiring, unauthenticated caller): - **Arbitrary internal reachability.** The attacker picks the destination and the server issues the request from inside the trust boundary. Delivery to loopback and to a real RFC1918 LAN address was exercised at runtime; because the code applies no destination filtering of any kind, the same path reaches link-local / metadata (`169.254.169.254`) and other internal ranges wherever network routing permits. - **Allowlist + method bypass via redirect.** A `302` from the attacker's webhook reaches destinations a creation-time allowlist would block, and a `302` on a `POST` is followed as a `GET` — so GET-only endpoints such as the IMDS credentials path are reachable even though the sender only issues `POST`s. This is why a URL allowlist is not a sufficient fix; the guard must validate the resolved IP of every redirect hop. - **Data push into internal services.** On direct (non-redirected) delivery, each request carries the task payload in the body, so internal services that ingest POST data receive attacker-influenced content (basis for `I:L`). On a redirected request the body is dropped in the POST→GET conversion, so the body-push and the redirect-reach are separate capabilities, not simultaneous on one request. The sender discards the fetched response and exposes no caller-visible difference between a successful and a failed delivery (both paths return without surfacing status or body), so the base case reads back nothing — confidentiality is scored `C:N`. Read-back is limited, reach is not: the attacker still drives fully controlled requests to metadata and internal services and writes data into them. ## PoC Against a running server with push notifications enabled (JSON-RPC transport), using only the real production classes wired the default way: 1. Start a local listener standing in for an internal target (no real metadata service is touched). 2. Send one `SendMessage` that starts a long-running task, with the push config inlined and its `url` set to `http://<internal-target>/latest/meta-data/iam/security-credentials/`. Use `returnImmediately: true`, header `A2A-Version: 1.0`, and role `ROLE_USER`. The URL is stored verbatim. (Equivalent via `CreateTaskPushNotificationConfig` on an existing task.) 3. As the task emits state/artifact updates, the sender POSTs each one to the stored URL, from the server's own network position: ``` POST /latest/meta-data/iam/security-credentials/ HTTP/1.1 Host: 169.254.169.254 Content-Type: application/json {"artifactUpdate":{"taskId":"...","artifact":{"parts":[{"text":"Progress update 1/20"}]}, ...}} ``` One `SendMessage` produced multiple server-side POSTs to the attacker-chosen internal path. Verified on `main` (commit `72d220c`, JDK 21) and unchanged on `1.2.0.Final`. ## Remediation Validate the *resolved* IP of the target and of every redirect hop before connecting: reject loopback / RFC1918 / link-local / metadata / unique-local / unspecified / multicast; restrict the scheme to `http`/`https`; cap redirects; and resolve-and-pin to defeat DNS rebinding. Enable by default, with a documented opt-out for deployments that intentionally target internal webhooks. The Go A2A SDK ships a dial-time resolved-IP guard of this shape by default. **Workaround:** until a fixed release, block the host's outbound egress to the metadata endpoint and internal ranges at the network layer. ## References - CWE-918: Server-Side Request Forgery: https://cwe.mitre.org/data/definitions/918.html

References

External links from the advisory. They lead to third-party sites.