A2A Java SDK has Server-Side Request Forgery (SSRF) in push-notification delivery
What fixes it
- 1.3.0.Finalorg.a2aproject.sdk:a2a-java-sdk-server-common, mavenGitHub
<dependency><groupId>org.a2aproject.sdk</groupId><artifactId>a2a-java-sdk-server-common</artifactId><version>1.3.0.Final</version></dependency> - 1.3.0.Finalorg.a2aproject.sdk:a2a-java-sdk-server-common, MavenOSV
<dependency><groupId>org.a2aproject.sdk</groupId><artifactId>a2a-java-sdk-server-common</artifactId><version>1.3.0.Final</version></dependency>
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
---
- Ecosystem: Maven
- Affected package:
org.a2aproject.sdk:a2a-java-sdk-server-common(push-notification delivery) - Affected versions:
<= 1.2.0.Final(and1.2.1.Final-SNAPSHOTonmainprior 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
302from 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 POSTs. 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:
- Start a local listener standing in for an internal target (no real metadata service is touched).
- Send one
SendMessagethat 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.)
- 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
Text from GitHub Advisory Database
Affected versions 9
- org.a2aproject.sdk:a2a-java-sdk-server-common, maven< 1.3.0.FinalGitHub
- a2a-javaaffected: < 1.3.0.FinalCVE List
- org.a2aproject.sdk:a2a-java-sdk-server-common, Mavenfrom first release, fixed in 1.3.0.FinalOSV
- org.a2aproject.sdk:a2a-java-sdk-server-common, Maven1.0.0.Alpha4OSV
- org.a2aproject.sdk:a2a-java-sdk-server-common, Maven1.0.0.Beta1OSV
- org.a2aproject.sdk:a2a-java-sdk-server-common, Maven1.0.0.CR1OSV
- org.a2aproject.sdk:a2a-java-sdk-server-common, Maven1.0.0.FinalOSV
- org.a2aproject.sdk:a2a-java-sdk-server-common, Maven1.1.0.FinalOSV
- org.a2aproject.sdk:a2a-java-sdk-server-common, Maven1.2.0.FinalOSV
References 6
- github.com/a2aproject/a2a-java/commit/b855b2ae37e4bf23f73e7746ce166fbc5a0b8b5bFixvia CVE List
- github.com/a2aproject/a2a-java/releases/tag/v1.3.0.FinalFixvia CVE List
- github.com/advisories/GHSA-q78c-5jjq-57g8Advisoryvia GitHub
- github.com/a2aproject/a2a-java/security/advisories/GHSA-q78c-5jjq-57g8Exploitvia CVE List
- nvd.nist.gov/vuln/detail/CVE-2026-111761Referencevia GitHub
- github.com/a2aproject/a2a-java/pull/1096Referencevia CVE List