Severity by source
AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:N
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:N
Lifecycle Timeline
3DescriptionGitHub Advisory
Summary
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.
Background
In the v1 auth flow (Nuts RFC003), access tokens are JWTs signed by the authorizer's key with:
iss= authorizer organization DIDsub= requester organization DIDservice= purpose of use (e.g."eOverdracht")typheader ="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).
Vulnerability details
The introspection endpoint performs only standard JWT checks. It does not perform the following Nuts-specific access token checks:
- Validate the
typheader: both ATs and VPs use"JWT" - Bind
issto the signing key: it doesn't verify that theissclaim matches the DID extracted from thekid - Validate required claims:
servicecan be empty;vpclaim is silently ignored byFromMap()which uses lenient JSON unmarshaling
Attack scenario
Prerequisites: Attacker (Org B) has received a VP JWT from the victim (Org A) during a normal access token request flow.
- Org A creates a VP JWT signed with Org A's key and sends it to Org B (normal protocol flow) to request an access token
- Org B presents this VP JWT to Org A's resource server as a bearer access token
- Resource server calls Org A's v1 introspection endpoint
- Introspection checks
privateKeyStore.Exists(kid), which passes, because Org A's key is on Org A's node - JSON unmarshaling is lenient; the
vpclaim is silently ignored - Returns
active: truewithservice: "",iss: "",sub: <Org A's DID>
Mitigating factors
serviceis empty: resource servers that strictly require a non-emptyservicefield may reject the request at the application levelissis empty: VP JWTs don't setiss, so resource servers checking this field would see an empty value- Short-lived VPs: VPs typically expire within minutes, narrowing the attack window
- v1 is legacy: the v2 flow uses opaque access tokens and is not affected
Severity rationale
While 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.
The fix
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-kidbinding: extract the DID from thekidheader and verify it matches theissclaim- Required claims validation: reject tokens where
serviceis empty typheader validation: requires access tokens to be oftyp: "at+jwt"
Additionally, the access token creation code has been updated to use typ: "at+jwt" per RFC 9068.
Patch
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.
Workaround
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).
AnalysisAI
JWT type confusion in Nuts Node v1 access token introspection endpoint allows authenticated attackers to replay Verifiable Presentation (VP) JWTs as access tokens, receiving active:true responses without proper validation of JWT type, issuer-to-key binding, or required claims. Exploitation requires prior receipt of a VP JWT during legitimate protocol flow and low attack complexity due to lenient JWT processing, though real-world impact is constrained by resource servers requiring valid service, iss, and aud fields for routing. Confirmed patched in v5.4.31 and v6.2.3.
Technical ContextAI
The vulnerability exploits a JWT type confusion pattern in the Nuts Node OAuth2 v1 authorization flow (RFC003). Access tokens and Verifiable Presentations both use typ:JWT header according to W3C VC Data Model 1.1, whereas W3C VC Data Model 2.0 explicitly changed VP typ to vp+jwt to prevent exactly this attack. The introspection endpoint (/auth/v1/introspect_access_token) performs only generic JWT signature validation against keys stored in privateKeyStore, but fails to enforce Nuts-specific controls: (1) typ header validation to distinguish AT (typ:JWT) from VP (typ:JWT), (2) binding of iss claim to kid-extracted DID to prevent key misuse, and (3) validation that required claims (service) are non-empty. The lenient JSON unmarshaling in FromMap() silently ignores unrecognized vp claims, allowing a VP structure to pass through without explicit rejection.
RemediationAI
Upgrade to Nuts Node v5.4.31 or v6.2.3 or later immediately. These releases add three validation controls to IntrospectAccessToken: (1) typ header validation requiring typ:at+jwt for access tokens, (2) iss-to-kid binding verification extracting DID from kid header and matching it to iss claim, and (3) required claims validation rejecting tokens with empty service field. Additionally, access token creation code updated to use typ:at+jwt per RFC 9068. If immediate upgrade is not possible, resource servers can implement compensating controls by explicitly validating introspection responses: reject any response where service is empty, reject responses where iss is empty or does not match expected authorizer DID, and reject responses where sub does not match expected requester organization DID. These workarounds add processing overhead but provide functional protection while patching is planned. See patches 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.
Same technique Information Disclosure
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-31940
GHSA-9hmg-827w-9rhj