The v1 access token introspection endpoint (/auth/v1/introspect_access_token) accepts any JWT signed by a key present on the node, without validating the JWT type, issuer-to-key binding, or required claims. This allows a Verifiable Presentation (VP) JWT to be replayed as an access token and receive an active: true introspection response.
In the v1 auth flow (Nuts RFC003), access tokens are JWTs signed by the authorizer's key with:
- iss = authorizer organization DID
- sub = requester organization DID
- service = purpose of use (e.g. "eOverdracht")
- typ header = "JWT" (default, not explicitly set)
Verifiable Presentations are also JWTs with typ: "JWT" (per W3C VC Data Model 1.1). The W3C VC Data Model 2.0 changed this to vp+jwt specifically to prevent this class of confusion attack (See Securing Verifiable Credentials using JOSE and COSE 3.1.1).
The introspection endpoint performs only standard JWT checks. It does not perform the following Nuts-specific access token checks:
typ header: both ATs and VPs use "JWT"iss to the signing key: it doesn't verify that the iss claim matches the DID extracted from the kidservice can be empty; vp claim is silently ignored by FromMap() which uses lenient JSON unmarshalingPrerequisites: Attacker (Org B) has received a VP JWT from the victim (Org A) during a normal access token request flow.
privateKeyStore.Exists(kid), which passes, because Org A's key is on Org A's nodevp claim is silently ignoredactive: true with service: "", iss: "", sub: <Org A's DID>service is empty: resource servers that strictly require a non-empty service field may reject the request at the application leveliss is empty: VP JWTs don't set iss, so resource servers checking this field would see an empty valueWhile the introspection endpoint incorrectly returns active: true for a replayed VP, we consider this not practically exploitable in the current deployment landscape. Resource servers require valid service, iss and aud values to route requests to the correct databases. A replayed VP returns empty service, empty iss, and wrong sub (Org A instead of B), making it unusable for meaningful access. The attack also requires the victim to first present a VP to the attacker through a legitimate protocol flow, and VPs are short-lived.
The severity reflects that the protection against exploitation is accidental (resource servers need service for routing, not for security) and we cannot guarantee how all resource server implementations handle the active: true response with missing fields.
Affected versions: all v5.x releases prior to v5.4.31, and all v6.x releases prior to v6.2.3. From v5.4.31 and v6.2.3 onward, the following checks have been added to IntrospectAccessToken:
iss-to-kid binding: extract the DID from the kid header and verify it matches the iss claimservice is emptytyp header validation: requires access tokens to be of typ: "at+jwt"Additionally, the access token creation code has been updated to use typ: "at+jwt" per RFC 9068.
Patches are available at https://github.com/nuts-foundation/nuts-node/releases/tag/v5.4.31 and https://github.com/nuts-foundation/nuts-node/releases/tag/v6.2.3.
If users are unable to update their nuts-node, resource servers can mitigate this risk by explicitly validating the introspection response: reject responses where service is empty, where iss is empty or does not match the expected authorizer DID, or where sub does not match the expected requester DID (Org B instead of A).
{
"nvd_published_at": null,
"severity": "MODERATE",
"cwe_ids": [
"CWE-345"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-05T17:15:32Z"
}