{"id":"GHSA-q78c-5jjq-57g8","summary":"A2A Java SDK has Server-Side Request Forgery (SSRF) in push-notification delivery","details":"---\n\n- **Ecosystem:** Maven\n- **Affected package:** `org.a2aproject.sdk:a2a-java-sdk-server-common` (push-notification delivery)\n- **Affected versions:** `\u003c= 1.2.0.Final` (and `1.2.1.Final-SNAPSHOT` on `main` prior to the fix)\n- **Patched version:** _(the release that carries the fix)_\n- **CWE:** CWE-918 (Server-Side Request Forgery)\n- **Severity:** High\n- **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)\n\n---\n\n## Summary\n\nAn **unauthenticated** attacker can coerce the a2a-java server into issuing HTTP requests to\narbitrary internal and cloud-metadata endpoints from the server's own network position — a\nServer-Side Request Forgery reachable in the default deployment with no credentials, no user\ninteraction, and a single request. The behaviour is the shipped default, not a misconfiguration.\n\n## Details\n\n`BasePushNotificationSender` (in `server-common`) POSTs task data to the URL stored on a task's\n`TaskPushNotificationConfig` with no validation of the destination: no block on loopback, RFC1918,\nlink-local (`169.254.0.0/16`, including the `169.254.169.254` metadata address), unique-local,\nunspecified, or multicast ranges; no scheme restriction; and no bound on redirects.\n`InMemoryPushNotificationConfigStore.setInfo` stores the URL verbatim and\n`onCreateTaskPushNotificationConfig` does not check it either.\n\nBoth shipped HTTP clients follow redirects (`JdkA2AHttpClient` → `followRedirects(NORMAL)`,\n`VertxA2AHttpClient` → `setFollowRedirects(true)`), so a creation-time URL allowlist is bypassable:\nan attacker webhook answers `302 Location: http://169.254.169.254/...` and the client follows it. A\nhostname check is likewise defeated by DNS rebinding. A correct guard must validate the *resolved*\nIP of every hop at connection time, not the URL string.\n\n## Exploitation\n\nDemonstrated end-to-end against a running server (default wiring, unauthenticated caller):\n\n- **Arbitrary internal reachability.** The attacker picks the destination and the server issues\n  the request from inside the trust boundary. Delivery to loopback and to a real RFC1918 LAN\n  address was exercised at runtime; because the code applies no destination filtering of any kind,\n  the same path reaches link-local / metadata (`169.254.169.254`) and other internal ranges\n  wherever network routing permits.\n- **Allowlist + method bypass via redirect.** A `302` from the attacker's webhook reaches\n  destinations a creation-time allowlist would block, and a `302` on a `POST` is followed as a\n  `GET` — so GET-only endpoints such as the IMDS credentials path are reachable even though the\n  sender only issues `POST`s. This is why a URL allowlist is not a sufficient fix; the guard must\n  validate the resolved IP of every redirect hop.\n- **Data push into internal services.** On direct (non-redirected) delivery, each request carries\n  the task payload in the body, so internal services that ingest POST data receive\n  attacker-influenced content (basis for `I:L`). On a redirected request the body is dropped in the\n  POST→GET conversion, so the body-push and the redirect-reach are separate capabilities, not\n  simultaneous on one request.\n\nThe sender discards the fetched response and exposes no caller-visible difference between a\nsuccessful and a failed delivery (both paths return without surfacing status or body), so the base\ncase reads back nothing — confidentiality is scored `C:N`. Read-back is limited, reach is not: the\nattacker still drives fully controlled requests to metadata and internal services and writes data\ninto them.\n\n## PoC\n\nAgainst a running server with push notifications enabled (JSON-RPC transport), using only the real\nproduction classes wired the default way:\n\n1. Start a local listener standing in for an internal target (no real metadata service is touched).\n2. Send one `SendMessage` that starts a long-running task, with the push config inlined and its\n   `url` set to `http://\u003cinternal-target\u003e/latest/meta-data/iam/security-credentials/`. Use\n   `returnImmediately: true`, header `A2A-Version: 1.0`, and role `ROLE_USER`. The URL is stored\n   verbatim. (Equivalent via `CreateTaskPushNotificationConfig` on an existing task.)\n3. As the task emits state/artifact updates, the sender POSTs each one to the stored URL, from the\n   server's own network position:\n\n   ```\n   POST /latest/meta-data/iam/security-credentials/ HTTP/1.1\n   Host: 169.254.169.254\n   Content-Type: application/json\n\n   {\"artifactUpdate\":{\"taskId\":\"...\",\"artifact\":{\"parts\":[{\"text\":\"Progress update 1/20\"}]}, ...}}\n   ```\n\nOne `SendMessage` produced multiple server-side POSTs to the attacker-chosen internal path.\nVerified on `main` (commit `72d220c`, JDK 21) and unchanged on `1.2.0.Final`.\n\n## Remediation\n\nValidate the *resolved* IP of the target and of every redirect hop before connecting: reject\nloopback / RFC1918 / link-local / metadata / unique-local / unspecified / multicast; restrict the\nscheme to `http`/`https`; cap redirects; and resolve-and-pin to defeat DNS rebinding. Enable by\ndefault, with a documented opt-out for deployments that intentionally target internal webhooks. The\nGo A2A SDK ships a dial-time resolved-IP guard of this shape by default.\n\n**Workaround:** until a fixed release, block the host's outbound egress to the metadata endpoint\nand internal ranges at the network layer.\n\n## References\n\n- CWE-918: Server-Side Request Forgery: https://cwe.mitre.org/data/definitions/918.html","aliases":["CVE-2026-111761"],"modified":"2026-10-20T21:15:04.059690010Z","published":"2026-10-20T21:06:22Z","database_specific":{"github_reviewed_at":"2026-10-20T21:06:22Z","nvd_published_at":"2026-10-20T15:17:11Z","cwe_ids":["CWE-918"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/a2aproject/a2a-java/security/advisories/GHSA-q78c-5jjq-57g8"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-111761"},{"type":"WEB","url":"https://github.com/a2aproject/a2a-java/pull/1096"},{"type":"WEB","url":"https://github.com/a2aproject/a2a-java/commit/b855b2ae37e4bf23f73e7746ce166fbc5a0b8b5b"},{"type":"PACKAGE","url":"https://github.com/a2aproject/a2a-java"},{"type":"WEB","url":"https://github.com/a2aproject/a2a-java/releases/tag/v1.3.0.Final"}],"affected":[{"package":{"name":"org.a2aproject.sdk:a2a-java-sdk-server-common","ecosystem":"Maven","purl":"pkg:maven/org.a2aproject.sdk/a2a-java-sdk-server-common"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.3.0.Final"}]}],"versions":["1.0.0.Alpha4","1.0.0.Beta1","1.0.0.CR1","1.0.0.Final","1.1.0.Final","1.2.0.Final"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-q78c-5jjq-57g8/GHSA-q78c-5jjq-57g8.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N"}]}