☠️
EXFILTRATING
DATA...
01010111 10110010 00101001 11000110 01101100 10011011 00010001 11100101 01000010 10101111
[ <-- BACK_TO_SEARCH ]

CVE-2025-31123

CVE-2025-31123 Supply Chain Attack in Zitadel
📅 PUBLISHED: 2025-03-31 19:31 UTC 🔄 UPDATED: 2026-10-11 17:42 UTC
[ PATCH UNDER REVIEW ↓ ] Remains UNDER REVIEW until an independent source confirms the fix
[ DEFENSE PATCH ↓ ]
💣 PoC: None
👁️
Proof of Concept - Indicates if functional exploit code is publicly available, drastically increasing the risk of immediate attacks.
🎯 EPSS: 0.40%
👁️
0.40% probability
9.9× above the median CVE
Tier: ELEVATED
30-day exploitation window · FIRST.org · updated daily
VULNERABLE: version not auto-detected — verify your deployed version locally
CVSS SEVERITY SCALE
9.0–10.0 CRITICAL
7.0–8.9 HIGH
4.0–6.9 MEDIUM
0.1–3.9 LOW
0.0 NONE
8.7
THREAT LEVEL
■ tracking this CVE ✓ independent patch confirmation ⧗ same-origin mirror (GHSA) ⚠ active exploitation verification policy →

Executive Summary

CVE-2025-31123 is a critical vulnerability in Zitadel, an open-source identity infrastructure platform. The flaw resides in the handling of JWT keys during Authorization Grants. Specifically, Zitadel fails to validate the expiration date of a JWT key when processing an Authorization Grant request. This allows an attacker who possesses an expired, but previously valid, JWT signing key to mint new, valid access tokens. The vulnerability does not affect the Token or Introspection endpoints, which correctly reject expired keys. The CVSS score of 8.7 (High) reflects the ease of exploitation (network-based, no authentication required) and the high impact on confidentiality, integrity, and availability. Patches are available across multiple release branches.

⚡ AUTOMATE YOUR CVE INTELLIGENCE

This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.

›Patch confidence — UNVERIFIED to CONFIRMED, cross-referenced NVD + RHSA + Debian
›Exploit & PoC — poc_status, poc_url, has_exploit. Not in NVD.
›Shodan dorks — ready-made queries to find exposed vulnerable systems
›Detection rules — YARA · Wazuh · Elastic (all plans) · Sigma (Pro)
›pending_fields — know exactly which of 7 fields NVD hasn't processed yet
›Free backfill — data incomplete when you paid? Re-check within 30 days = 0 credits
PAYG €0.08/lookup Standard €19/mo (was €29) · 5k credits Pro €65/mo (was €99) · 20k credits FOUNDING PRICE · LOCKED FOR LIFE · ENDS 31 DEC 2026
[ FROM €19/MO ]
Free sandbox key available
No signup required

Supply Chain & Package Security

Zitadel is distributed as a Go binary and Docker container. The vulnerability is in the core server logic, not in a specific package dependency. However, organizations using Zitadel as a dependency in their own Go projects should update their go.mod to reference a patched version. The fix is included in the official Zitadel Docker images and binary releases.

CVSS Breakdown & Threat Level

  • Attack Vector (AV): Network (N) - The vulnerability can be exploited remotely.
  • Attack Complexity (AC): Low (L) - No special conditions are required.
  • Privileges Required (PR): None (N) - The attacker does not need any prior authentication.
  • User Interaction (UI): None (N) - No user action is required.
  • Scope (S): Unchanged (U) - The exploit does not affect other components.
  • Confidentiality (C): High (H) - An attacker can obtain valid access tokens, potentially gaining access to protected resources.
  • Integrity (I): High (H) - The attacker can forge tokens, leading to unauthorized data modification.
  • Availability (A): High (H) - The attacker can disrupt service by generating tokens that consume resources or bypass access controls.

Threat Level: Critical. The combination of network exploitability, no required privileges, and high impact makes this a top-priority vulnerability for any organization using Zitadel.

Deep Dive & Mechanics

The vulnerability is rooted in the Authorization Grant flow (RFC 7523). In this flow, a client presents a JWT (signed by a key it possesses) to the token endpoint to obtain an access token. Zitadel's implementation incorrectly checks the validity of the JWT key (the key used to sign the JWT) rather than the JWT itself. The key's expiration date is not validated.

Normal Flow:

  1. Client generates a JWT assertion, signed with its private key.
  2. Client sends the JWT to Zitadel's token endpoint with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer.
  3. Zitadel retrieves the client's public key (by kid or other identifier).
  4. Zitadel verifies the JWT signature using the public key.
  5. Zitadel checks the JWT's exp (expiration) claim.
  6. If valid, Zitadel issues an access token.

Vulnerable Flow:

  1. Attacker obtains an expired private key (e.g., from a leaked backup, a former employee, or a compromised system).
  2. Attacker generates a JWT assertion with a future exp claim, signed with the expired key.
  3. Attacker sends the JWT to Zitadel's token endpoint.
  4. Zitadel retrieves the public key (which is also expired).
  5. Zitadel verifies the JWT signature (passes, as the key is still cryptographically valid).
  6. Zitadel does NOT check if the public key itself is expired.
  7. Zitadel checks the JWT's exp claim (passes, as the attacker set it to a future time).
  8. Zitadel issues a valid access token.

The key distinction is that the JWT's exp claim is checked, but the key's own expiration (if any) is not. This allows an attacker to reuse a key that should have been revoked.

Vulnerable vs. Patched Code

The fix involves adding a check for the key's expiration date in the Authorization Grant handler.

Vulnerable Code (Conceptual Go):

func handleAuthorizationGrant(r *http.Request) {
    // ... parse JWT, extract kid ...
    key := getPublicKey(kid)
    // Missing: check if key is expired
    err := jwt.Verify(jwtString, key)
    if err != nil {
        // return error
    }
    // ... check JWT exp ...
    // issue token
}

Patched Code (Conceptual Go):

func handleAuthorizationGrant(r *http.Request) {
    // ... parse JWT, extract kid ...
    key := getPublicKey(kid)
    if key.Expiry != nil && time.Now().After(key.Expiry) {
        // return error: key is expired
    }
    err := jwt.Verify(jwtString, key)
    if err != nil {
        // return error
    }
    // ... check JWT exp ...
    // issue token
}

The patched code explicitly checks key.Expiry before proceeding with signature verification.

Exploitation & Lab Setup

Red Team

Prerequisites:

  • A Zitadel instance (vulnerable version).
  • An expired JWT signing key (private key) that was previously valid for a client.
  • The corresponding public key (or ability to derive it).

Exploitation Steps:

  1. Obtain Expired Key: Acquire an expired private key. This could be from a leaked configuration, a former employee's machine, or a compromised CI/CD pipeline.
  2. Craft JWT Assertion: Create a JWT with the following claims:
    • iss: The client ID associated with the key.
    • sub: The client ID.
    • aud: The Zitadel token endpoint URL (e.g., https://zitadel.example.com/oauth/v2/token).
    • exp: A future timestamp (e.g., 1 hour from now).
    • iat: A current timestamp.
    • jti: A unique identifier.
    • kid: The key ID of the expired key.
  3. Sign JWT: Sign the JWT using the expired private key.
  4. Send Request:
    curl -X POST "https://zitadel.example.com/oauth/v2/token" \
         -H "Content-Type: application/x-www-form-urlencoded" \
         -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
         -d "assertion=<JWT>"
    
  5. Receive Access Token: The response will contain a valid access_token that can be used to access protected resources.

Example JWT (using jwt.io or a script):

{
  "alg": "RS256",
  "kid": "expired-key-id",
  "typ": "JWT"
}
.
{
  "iss": "client-id",
  "sub": "client-id",
  "aud": "https://zitadel.example.com/oauth/v2/token",
  "exp": 1893456000,
  "iat": 1700000000,
  "jti": "unique-id-123"
}

Blue Team

Detection:

  • Monitor for Authorization Grant requests (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer) that use keys that are known to be expired.
  • Alert on any successful token issuance where the key's exp date is in the past.
  • Review logs for kid values that are not in the current, active key set.

Hunting:

  • Query Zitadel audit logs for AUTHZ_GRANT_SUCCESS events.
  • Cross-reference the kid used in the request with the key's expiration date in the Zitadel key store.
  • Look for patterns of token issuance from a single client using multiple different kid values in a short time.

Prevention:

  • Immediately apply the patch.
  • Rotate all client secrets and keys.
  • Implement key expiration policies and ensure keys are properly revoked.
  • Use short-lived keys and rotate them frequently.

Indicators of Compromise (IOCs)

  • Network:
    • POST requests to /oauth/v2/token with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer and a kid that is not in the current active key set.
    • JWT assertions with an exp claim that is far in the future (e.g., years ahead).
  • Logs:
    • Zitadel audit log entries for AUTHZ_GRANT_SUCCESS where the key's exp date is before the request timestamp.
    • Repeated failed attempts with different kid values (if attacker is probing).
  • System:
    • Presence of expired private key files on disk (e.g., in /etc/zitadel/keys/ or similar).
    • Unauthorized access tokens being used against protected APIs.

Detection Engineering

YARA Rule

rule CVE_2025_31123_Zitadel_Expired_Key {
    meta:
        description = "Detects JWT assertions that may be exploiting CVE-2025-31123 by using an expired key"
        author = "Valters IT"
        date = "2025-04-10"
        reference = "CVE-2025-31123"
    strings:
        $jwt_header = /{"alg":"[A-Za-z0-9]+","kid":"[A-Za-z0-9_-]+","typ":"JWT"}/
        $grant_type = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"
        $assertion = "assertion="
    condition:
        $grant_type and $assertion and #jwt_header > 0
}

Sigma Rule

title: CVE-2025-31123 - Zitadel Authorization Grant with Expired Key
id: 8a7b3c2d-1e5f-4a6b-9c8d-0e1f2a3b4c5d
status: experimental
description: Detects successful Authorization Grant requests in Zitadel where the key used is expired.
author: Valters IT
date: 2025/04/10
references:
    - https://nvd.nist.gov/vuln/detail/CVE-2025-31123
logsource:
    product: zitadel
    service: audit
detection:
    selection:
        event_type: AUTHZ_GRANT_SUCCESS
    condition: selection
falsepositives:
    - Legitimate use of a key that was recently rotated but not yet removed from the key store.
level: high

Suricata Rule

alert http $HOME_NET any -> $EXTERNAL_NET any (
    msg:"CVE-2025-31123 - Zitadel Authorization Grant with Expired Key";
    flow:to_server,established;
    content:"POST"; http_method;
    content:"/oauth/v2/token"; http_uri;
    content:"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"; http_client_body;
    content:"assertion="; http_client_body;
    pcre:"/kid\":\"[A-Za-z0-9_-]+\"/";
    sid:10000001;
    rev:1;
)

Remediation & Mitigation

Immediate Actions:

  1. Patch: Upgrade Zitadel to one of the fixed versions: 2.71.6, 2.70.8, 2.69.9, 2.68.9, 2.67.13, 2.66.16, 2.65.7, 2.64.6, or 2.63.9.
  2. Rotate Keys: After patching, rotate all client JWT signing keys. This ensures that any expired keys that may have been compromised are no longer valid.
  3. Revoke Tokens: Invalidate all access tokens that were issued using the vulnerable flow. This can be done by changing the signing key for access tokens or by using Zitadel's token revocation API.

Long-Term Mitigations:

  • Key Management: Implement a robust key lifecycle management process. Keys should be rotated regularly and have explicit expiration dates.
  • Monitoring: Set up alerts for any Authorization Grant requests that use keys with an expiration date in the past.
  • Audit: Regularly audit key usage and ensure that expired keys are removed from the key store.
  • Network Segmentation: Restrict access to the Zitadel token endpoint to only trusted clients.

Workarounds (if patching is not immediately possible):

  • Disable the Authorization Grant flow (urn:ietf:params:oauth:grant-type:jwt-bearer) if it is not required.
  • Implement a Web Application Firewall (WAF) rule to block requests with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer from untrusted IPs.

Valters IT

◈ SOURCE Analysis & enrichment: valtersit.com/cve/CVE-2025-31123/ · CVE data: NVD/MITRE (public domain) · © 2026 ValtersIT Hub
>_ SYSTEM_CORE: INTEL_ACTIVE
90% TEAM-CURATED // 10% AUTOMATED INGESTION.
ALL PATCH REFERENCES AND VERSION DATA SOURCED FROM NVD + OSV OFFICIAL REGISTRIES.