CVE-2025-31123
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.
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.
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:
- Client generates a JWT assertion, signed with its private key.
- Client sends the JWT to Zitadel's token endpoint with
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. - Zitadel retrieves the client's public key (by
kidor other identifier). - Zitadel verifies the JWT signature using the public key.
- Zitadel checks the JWT's
exp(expiration) claim. - If valid, Zitadel issues an access token.
Vulnerable Flow:
- Attacker obtains an expired private key (e.g., from a leaked backup, a former employee, or a compromised system).
- Attacker generates a JWT assertion with a future
expclaim, signed with the expired key. - Attacker sends the JWT to Zitadel's token endpoint.
- Zitadel retrieves the public key (which is also expired).
- Zitadel verifies the JWT signature (passes, as the key is still cryptographically valid).
- Zitadel does NOT check if the public key itself is expired.
- Zitadel checks the JWT's
expclaim (passes, as the attacker set it to a future time). - 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:
- 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.
- 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.
- Sign JWT: Sign the JWT using the expired private key.
- 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>" - Receive Access Token: The response will contain a valid
access_tokenthat 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
expdate is in the past. - Review logs for
kidvalues that are not in the current, active key set.
Hunting:
- Query Zitadel audit logs for
AUTHZ_GRANT_SUCCESSevents. - Cross-reference the
kidused 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
kidvalues 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/tokenwithgrant_type=urn:ietf:params:oauth:grant-type:jwt-bearerand akidthat is not in the current active key set. - JWT assertions with an
expclaim that is far in the future (e.g., years ahead).
- POST requests to
- Logs:
- Zitadel audit log entries for
AUTHZ_GRANT_SUCCESSwhere the key'sexpdate is before the request timestamp. - Repeated failed attempts with different
kidvalues (if attacker is probing).
- Zitadel audit log entries for
- 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.
- Presence of expired private key files on disk (e.g., in
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:
- 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.
- 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.
- 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-bearerfrom untrusted IPs.
Valters IT