Skip to content

[Security/Bug] Replay Attack vulnerability in OID4VP due to lack of Session Invalidation (Nonce reuse) #1771

Description

@minami-truong

Description:
In the current implementation of the Verifier API (POST /openid4vc/verify/{state}), a session remains active for its entire TTL (default 5 minutes) even after a Verifiable Presentation (VP) has been successfully verified.

Because the session is not locked, consumed, or invalidated upon the first successful verification, an attacker can capture the VP Token and resubmit it multiple times within the TTL window. Each submission yields a verificationResult = true and triggers the webhook (VerifierService.notifySubscribers).

This violates the fundamental cryptographic principle of a "nonce" (Number used ONCE) in OpenID4VP and RFC 6819 (OAuth 2.0 Threat Model - Mitigate Replay of Authorization Response), leaving downstream applications vulnerable to Replay Attacks if they do not implement their own distributed locking mechanisms.

Steps to Reproduce:

  1. Initialize a verification session via POST /openid4vc/verify and retrieve the session_id and nonce.
  2. Submit a valid VP Token (containing the correct signature and nonce) to POST /openid4vc/verify/{session_id}.
  3. Observe that the verification is successful and the webhook is triggered.
  4. Immediately submit the exact same VP Token again to the same endpoint within the 5-minute TTL.
  5. Observe that the API accepts the payload, returns a successful validation, and triggers the webhook again.

Expected Behavior:
According to OID4VP standards, once a VP Token is successfully verified against a specific session/nonce, that session MUST be immediately marked as consumed/resolved. Any subsequent attempts to submit to the same session should be rejected (e.g., HTTP 400 or 403) to prevent Replay Attacks.

Actual Behavior:
Based on the source code in OpenIDCredentialVerifier.kt (inside the verify function), the session state is updated via putSession but there is no mechanism to prevent re-verification if the verificationResult is already true.

// In OpenIDCredentialVerifier.kt
return session.copy(
    tokenResponse = tokenResponse,
    verificationResult = doVerify(...)
).also {
    putSession(id = it.id, session = it, ttl = remainingTtl) // Session is updated, but not locked/consumed
}

Suggested Fix:
Implement a state lock or a status flag (e.g., PENDING, RESOLVED, FAILED). If doVerify() evaluates to true, the session status should transition to RESOLVED and the nonce should be considered burned. Any incoming request for a RESOLVED session should be immediately rejected.

Environment:

  • Repository: waltid-identity
  • Branch: main
  • Module: VerifierService, OpenIDCredentialVerifier

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions