CVE-2026-55414: nl-portal: GraphQL SSRF exposes Objecten-API auth token

GHSA-xm3x-9cfw-jhx4 MEDIUM
Published June 19, 2026
CISO Take

An unauthenticated attacker can POST to the public /graphql endpoint of nl-portal-backend-libraries and invoke two form-definition resolvers that forward a caller-supplied URL directly to the privileged Objecten-API HTTP client, which automatically attaches the configured bearer token to every outbound request. While the SSRF is constrained to the configured Objecten-API host — extensive URL-parser differential testing found no bypass — practical token capture is possible against any deployment where the attacker controls a listener at a different port or path on that same host (multi-tenant environments, shared infrastructure). With 873 downstream dependents, an OpenSSF Scorecard of 5.7/10, and 11 prior CVEs in the package family, upgrade nl.nl-portal:* to 3.0.4.RELEASE and nl-portal-frontend-libraries to v3.0.3 immediately; the fix removes the vulnerable resolvers entirely and is a breaking change requiring coordinated backend and frontend upgrades.

Sources: GitHub Advisory NVD OpenSSF ATLAS

What is the risk?

Medium. Exploitation requires no authentication and trivial tooling — a single GraphQL POST — but practical impact is bounded by the same-host SSRF constraint. No public exploit code exists, CISA has not listed this in KEV, and no EPSS data is available at time of analysis. The elevated concern is structural: the OpenSSF score of 5.7 and 11 historical CVEs in the same package indicate persistent security hygiene gaps, and the affected system processes Dutch government citizen data (Objecten-API objects include BSN-linked task records and zaken). Deployments in shared-cloud or co-hosted environments face elevated risk where the configured Objecten-API host shares infrastructure with attacker-accessible services on alternate ports.

How does the attack unfold?

Initial Access
Attacker confirms /graphql is permitAll() by sending an unauthenticated introspection or resolver query, then calls getFormDefinitionByObjectenApiUrl with a crafted URL targeting a non-standard path on the configured Objecten-API host.
AML.T0049
SSRF Traversal
The caller-supplied URL propagates unchecked through FormDefinitionQuery → ObjectsApiFormDefinitionService → ObjectenApiService, where only a host equality check (no scheme, port, or path validation) occurs before the WebClient issues the outbound request.
AML.T0049
Credential Capture
The backend's HTTP client attaches Authorization: Token <objecten-api-token> to the outbound request reaching the attacker-controlled listener at an alternate port on the same host, delivering the privileged token out-of-band.
AML.T0106
Data Exfiltration
Armed with the captured Objecten-API token, the attacker queries the API directly for citizen records, BSN-linked tasks, and case files — bypassing the portal's typed deserialization boundary that constrained what the GraphQL resolver could return.
AML.T0055

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Ray maven >= 1.1.0, < 3.0.4 3.0.4
42.9K OpenSSF 5.7 873 dependents Pushed 5d ago 83% patched ~139d to patch Full package profile →

Do you use Ray? You're affected.

How severe is it?

CVSS 3.1
5.3 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

What is the attack surface?

AV AC PR UI S C I A
AV Network
AC Low
PR None
UI None
S Unchanged
C Low
I None
A None

What should I do?

6 steps
  1. Upgrade nl.nl-portal:* (all modules) to 3.0.4.RELEASE or later — the fix removes getFormDefinitionByObjectenApiUrl and getFormDefinitionById entirely and replaces URL-based lookup with UUID-based retrieval.

  2. Simultaneously upgrade nl-portal-frontend-libraries to v3.0.3+ — mandatory, not optional, as removed GraphQL queries are a breaking API contract change.

  3. If immediate patching is blocked, apply a WAF/reverse-proxy rule blocking POST /graphql requests containing 'getFormDefinitionByObjectenApiUrl' or 'getFormDefinitionById' as interim control.

  4. Rotate the Objecten-API token in all environments regardless of confirmed exploitation; treat it as potentially compromised.

  5. Audit /graphql access logs for POST requests with resolver names and non-standard URL argument values predating the patch.

  6. For the ZakenApiService path traversal defense gap, verify that getZaakDetails remains gated by CommonGroundAuthentication after the upgrade.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 9 - Risk management system
ISO 42001
A.6.2.6 - AI system access control
NIST AI RMF
GOVERN 1.1 - Policies and processes across AI risk areas MANAGE 2.4 - Residual risks and vulnerabilities

Frequently Asked Questions

What is CVE-2026-55414?

An unauthenticated attacker can POST to the public /graphql endpoint of nl-portal-backend-libraries and invoke two form-definition resolvers that forward a caller-supplied URL directly to the privileged Objecten-API HTTP client, which automatically attaches the configured bearer token to every outbound request. While the SSRF is constrained to the configured Objecten-API host — extensive URL-parser differential testing found no bypass — practical token capture is possible against any deployment where the attacker controls a listener at a different port or path on that same host (multi-tenant environments, shared infrastructure). With 873 downstream dependents, an OpenSSF Scorecard of 5.7/10, and 11 prior CVEs in the package family, upgrade nl.nl-portal:* to 3.0.4.RELEASE and nl-portal-frontend-libraries to v3.0.3 immediately; the fix removes the vulnerable resolvers entirely and is a breaking change requiring coordinated backend and frontend upgrades.

Is CVE-2026-55414 actively exploited?

No confirmed active exploitation of CVE-2026-55414 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-55414?

1. Upgrade nl.nl-portal:* (all modules) to 3.0.4.RELEASE or later — the fix removes getFormDefinitionByObjectenApiUrl and getFormDefinitionById entirely and replaces URL-based lookup with UUID-based retrieval. 2. Simultaneously upgrade nl-portal-frontend-libraries to v3.0.3+ — mandatory, not optional, as removed GraphQL queries are a breaking API contract change. 3. If immediate patching is blocked, apply a WAF/reverse-proxy rule blocking POST /graphql requests containing 'getFormDefinitionByObjectenApiUrl' or 'getFormDefinitionById' as interim control. 4. Rotate the Objecten-API token in all environments regardless of confirmed exploitation; treat it as potentially compromised. 5. Audit /graphql access logs for POST requests with resolver names and non-standard URL argument values predating the patch. 6. For the ZakenApiService path traversal defense gap, verify that getZaakDetails remains gated by CommonGroundAuthentication after the upgrade.

What systems are affected by CVE-2026-55414?

This vulnerability affects the following AI/ML architecture patterns: government citizen portals, GraphQL API backends with unauthenticated public resolvers, Common Ground API integrations, multi-service internal API meshes with shared-token authentication.

What is the CVSS score for CVE-2026-55414?

CVE-2026-55414 has a CVSS v3.1 base score of 5.3 (MEDIUM).

What is the AI security impact?

Affected AI Architectures

government citizen portalsGraphQL API backends with unauthenticated public resolversCommon Ground API integrationsmulti-service internal API meshes with shared-token authentication

MITRE ATLAS Techniques

AML.T0049 Exploit Public-Facing Application
AML.T0055 Unsecured Credentials
AML.T0106 Exploitation for Credential Access

Compliance Controls Affected

EU AI Act: Article 9
ISO 42001: A.6.2.6
NIST AI RMF: GOVERN 1.1, MANAGE 2.4

What are the technical details?

Original Advisory

## Summary The public GraphQL resolvers `getFormDefinitionByObjectenApiUrl(url)` and the deprecated `getFormDefinitionById(id)` fetch a caller-supplied URL using the **privileged Objecten-API token**. Because the `/graphql` endpoint is `permitAll()` and these resolvers do not declare a `CommonGroundAuthentication` parameter, an **unauthenticated** caller can make the backend issue an outbound request carrying `Authorization: Token <objecten-api-token>` to a **caller-influenced URL on the configured Objecten-API host**. This is a constrained (same-host) server-side request forgery combined with missing authorization. Reported responsibly and confirmed in a local lab build against the project's own WebFlux security stack. No production system was accessed. ## Affected - `nl.nl-portal:form` (the public resolver / entry point) together with `nl.nl-portal:objectenapi` (where the host guard lives). - First shipped in **1.1.0.RELEASE** (2023-10-31); the vulnerable code was introduced on 2023-08-12 (commit `b2f87ca`) and is present in every release since (1.1.x, 1.2.5, 1.3.0, the 3.0.x line, and 3.1.0 / `next-minor`, HEAD `45abcd2`). Fixed in 3.0.4.RELEASE (see Fix below). ## Data flow (confirmed in source) 1. `form/.../graphql/FormDefinitionQuery.kt` — `@QueryMapping getFormDefinitionByObjectenApiUrl(@Argument url)`, **no** `CommonGroundAuthentication` parameter (same for `getFormDefinitionById`). 2. → `form/.../service/ObjectsApiFormDefinitionService.kt` — passes the URL through unvalidated. 3. → `zgw/objectenapi/.../service/ObjectenApiService.kt` `getObjectByUrl(url)` — the only guard is host equality (`URI.create(url).host == objectsApiClientConfig.url.host`); **no scheme/port/path check**. 4. → `zgw/objectenapi/.../client/ObjectsApiClient.kt` `getObjectByUrl(url)` via `webClientWithoutBaseUrl()`, which attaches the default header `Authorization: Token <token>` to the fully caller-supplied URL. **Reachability:** `/graphql` is `permitAll()` (`core/.../security/OauthSecurityAutoConfiguration.kt`). Authentication is only enforced on resolvers that declare a `CommonGroundAuthentication` parameter; these do not, and there is no `@PreAuthorize`/instrumentation safety net. The project's own `GraphQLEndpointAuthorizationIT` lists `getFormDefinitionByObjectenApiUrl` as an intentionally public operation — so the unauthenticated reachability is by design; the defect is that an intentionally-public resolver forwards a privileged token to a caller-influenced URL. **Secondary (defense-in-depth):** `zgw/zaken-api/.../service/ZakenApiService.kt` `getZaakDetails` calls `objectsApiClient.getObjectByUrl` **directly**, bypassing the service-level host guard. It is currently only reachable via the authenticated `ZaakQuery.zaakdetails` field resolver with server-derived URLs, so it is not an unauthenticated vector today — but it shows why the guard belongs in the client. ## Proof of concept (lab, against the real WebFlux stack) - An unauthenticated `POST /graphql` calling `getFormDefinitionByObjectenApiUrl(url: ...)` executes without authentication. - With the configured Objecten-API host pointed at a mock server, an outbound request to a **caller-chosen port/path on that host** carried `Authorization: Token <configured-token>` — confirming the token is attached to caller-influenced URLs. ## Impact and severity — important limitations Assessed as **Medium** because two code-level facts constrain practical impact: 1. **No cross-host SSRF / token exfiltration in standard deployments.** The token only travels to the *configured* Objecten-API host. Exfiltration requires an attacker-controlled listener at that host (a different port/path routing elsewhere) — generally not the case in managed deployments. A range of URL-parser bypass payloads was tested (userinfo `@`, `%2f`/`%00`/`%09`, backslash, `#`/`?`, double-host, trailing-dot, IDN/Unicode full-stop, fraction-slash, IPv6); **no parser differential** was found between the `java.net.URI`-based guard and the Spring/Netty URI builder used by WebClient — every payload either kept the request on the configured host or was rejected (fail-closed). The lab token-leak PoC works only because the configured host there is `localhost`; this does not generalize to production. 2. **Arbitrary PII object read is blocked by typed deserialization.** The response is deserialized into `ObjectsApiObject<ObjectsApiFormIoFormDefinition>`, whose envelope fields and `data.formDefinition` are all non-nullable Kotlin properties (Jackson `KotlinModule` registered). An object without a top-level `data.formDefinition` (e.g. taken/berichten/zaakdetails) fails to deserialize (`DecodingException`) and returns no data. The resolver can therefore only return objects shaped like a form definition — and form definitions are intentionally public (loaded pre-login). **Escalation conditions** that would raise severity toward High: - the Objecten-API host shares infrastructure with an attacker-controllable endpoint (other port/path), enabling capture of the privileged token; or - a URL-parser differential is later found that escapes the host guard. ## Remediation - Move the host validation out of `ObjectenApiService.getObjectByUrl` and into `ObjectsApiClient.getObjectByUrl` so the direct caller `ZakenApiService.getZaakDetails` is covered too, and tighten it from host-only to **scheme + host + port + path-prefix**. Preferably, do not accept a full URL at all: validate/extract the object UUID and rebuild the URL from the fixed configured base (reuse the existing `ObjectsApiClient.getObjectById` pattern, `/api/v2/objects/{uuid}`). - Separately decide whether `getFormDefinitionByObjectenApiUrl` / `getFormDefinitionById` should remain unauthenticated. They are currently intentionally public (forms load before login); for a stricter posture, add a `CommonGroundAuthentication` parameter as in the other resolvers — noting this breaks pre-login form loading. ## Credit Reported responsibly by **Ray Sabee** (https://whitehatsecurity.nl), independent security researcher — GitHub [@raysabee](https://github.com/raysabee). ## Fix Fixed in **3.0.4.RELEASE** (commit `39ad80f`, PR #700, "rework form module"): - The unauthenticated resolvers `getFormDefinitionByObjectenApiUrl` and the deprecated `getFormDefinitionById` were **removed** from both `FormDefinitionQuery` and the GraphQL schema. - `getFormDefinitionByName` now requires a `CommonGroundAuthentication` parameter (no longer public). - The URL-based service method `findObjectsApiFormDefinitionByUrl(url)` was removed and replaced by `getObjectsApiFormDefinitionById(objectId: UUID)`, which fetches by UUID via the fixed `/api/v2/objects/{uuid}` path (no caller-supplied URL, so no SSRF) and validates the object type against the configured form-definition object type. - Form definitions are now retrieved through the new authenticated query `getFormDefinitionByTaskId(taskId)` in `nl.nl-portal:taak`, which authorizes the caller against the task (`CommonGroundAuthentication`, BSN/KVK match, else `401`) and derives the form-definition UUID from the task's own server-side data, not from caller input. - No resolver feeds caller-controlled input into `ObjectenApiService.getObjectByUrl` anymore. The `objectenapi` module itself was not changed; the fix lives entirely in `nl.nl-portal:form` and the new `nl.nl-portal:taak` query. ## Upgrade instructions - **Backend:** upgrade `nl.nl-portal:*` to **3.0.4** (or later). - **Frontend:** upgrade `nl-portal-frontend-libraries` to **v3.0.3** (or later). This is required: the removed GraphQL queries (`getFormDefinitionByObjectenApiUrl`, `getFormDefinitionById`) and the now-authenticated `getFormDefinitionByName` are a breaking change. Frontend v3.0.3 uses the new authenticated `getFormDefinitionByTaskId` / `getFormDefinitionByName` queries.

Exploitation Scenario

An attacker discovers a public-facing nl-portal citizen portal and confirms /graphql accepts unauthenticated POSTs. They craft a request calling getFormDefinitionByObjectenApiUrl with a URL pointing to a non-standard path or port on the configured Objecten-API host — for example, http://objecten-api.internal:9090/metrics. The backend validates only host equality (passes), then issues an outbound GET carrying Authorization: Token <configured-token> to the attacker-specified path. In a shared-tenant or misconfigured cloud environment where the attacker controls a service at an alternate port on the Objecten-API host, the token arrives at the attacker's listener. Armed with the privileged token, the attacker directly queries the Objecten-API for citizen task records, case files, and PII-bearing objects — entirely outside the portal's typed deserialization boundary that limited the GraphQL response.

Weaknesses (CWE)

CWE-862 — Missing Authorization: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.

  • [Architecture and Design] Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
  • [Architecture and Design] Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

Timeline

Published
June 19, 2026
Last Modified
June 19, 2026
First Seen
June 19, 2026

Related Vulnerabilities