Skip to content

CVE-2026-11800: Verified Repro With Script Download

CVE-2026-11800: Keycloak JWT algorithm confusion privilege escalation

CVE-2026-11800 is verified against keycloak/keycloak · github. Affected versions: Keycloak < 26.6.4 (26.6.x line). Fixed in 26.6.4. Vulnerability class: Privilege Escalation. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00243.

REPRO-2026-00243 keycloak/keycloak · github Privilege Escalation Variant found Jul 6, 2026 CVE entry ↗ .txt
Severity
HIGH
CVSS
8.1
Confidence
HIGH
Reproduced in
46m 0s
Tool calls
281
Spend
$7.18
01 · Overview

What Is CVE-2026-11800?

CVE-2026-11800 is a high-severity JWT algorithm confusion vulnerability (CWE-347) in Keycloak's JWT Authorization Grant flow that allows privilege escalation and impersonation. Pruva reproduced it (reproduction REPRO-2026-00243).

02 · Severity & CVSS

CVE-2026-11800 Severity & CVSS Score

CVE-2026-11800 is rated high severity, with a CVSS base score of 8.1 out of 10.

HIGH threat level
8.1 / 10 CVSS base
Weakness CWE-347 (Improper Verification of Cryptographic Signature)

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

Affected keycloak/keycloak Versions

keycloak/keycloak · github versions Keycloak < 26.6.4 (26.6.x line) are affected.

How to Reproduce CVE-2026-11800

$ pruva-verify REPRO-2026-00243
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00243/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-11800

Authorization bypass — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Forged HS256 JWT assertion signed with RSA public key DER bytes as HMAC secret, submitted via grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer

Attack chain
  1. POST /realms/{realm}/protocol/openid-connect/token with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer and assertion=<forged HS256 JWT>
Runnable proof: reproduction_steps.sh
Captured evidence: fixed keycloakfixed keycloak
Variants tested

Alternate trigger for CVE-2026-11800: JWT algorithm confusion (HS256 with RSA public key DER bytes as HMAC secret) against a hardcoded-public-key OIDC Identity Provider, reached via the OIDC external-internal token exchange (RFC 8693, grant_type=urn:ietf:params:oauth:grant-type:token-exchange, subject_token_type=urn:i…

How the agent worked 711 events · 281 tool calls · 46 min
46 minDuration
281Tool calls
201Reasoning steps
711Events
2Dead-ends
Agent activity over 46 min
Support
22
Hypothesis
2
Repro
351
Judge
31
Variant
301
0:0046:00

Root Cause and Exploit Chain for CVE-2026-11800

Versions: Red Hat Build of Keycloak 26.6.0–26.6.3; upstream Keycloak versions prior to the fix in 26.6.4/26.7.0Fixed: Keycloak 26.6.4, 26.7.0

CVE-2026-11800 is a JWT algorithm confusion vulnerability in Keycloak's JWT Authorization Grant flow (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer). When an Identity Provider is configured with a hardcoded public key (not via JWKS URL) and the jwtAuthorizationGrantAssertionSignatureAlg setting is left unset (the default), the AbstractBaseJWTValidator.validateSignatureAlgorithm() method does not reject symmetric algorithms (HS256/HS384/HS512). Additionally, the HardcodedPublicKeyLoader contains an HMAC branch that loads the configured publicKeySignatureVerifier value as an HMAC secret key using Base64Url.decode(). An attacker who knows the Identity Provider's RSA public key (which is inherently public) can forge a JWT assertion with alg: HS256, sign it using the RSA public key's DER-encoded bytes as the HMAC secret, and submit it to Keycloak's token endpoint. The vulnerable Keycloak version accepts this forged assertion and issues a valid access token for any federated user linked to the affected Identity Provider, enabling privilege escalation and impersonation. The fix (PR #50374, commit d343c7373dcc7bfeb9dc14de2309836299c81837) adds explicit algorithm-type validation that rejects symmetric algorithms in JWT public key validators and removes the HMAC branch from HardcodedPublicKeyLoader.

  • Package/component affected: org.keycloak:keycloak-servicesAbstractBaseJWTValidator, HardcodedPublicKeyLoader, JWTAuthorizationGrantValidator, JWTAuthorizationGrantIdentityProvider
  • Affected versions: Red Hat Build of Keycloak 26.6.0–26.6.3; upstream Keycloak versions prior to the fix in 26.6.4/26.7.0
  • Fixed versions: Keycloak 26.6.4, 26.7.0
  • Risk level: High
  • Consequences: An attacker with valid client credentials can bypass JWT signature verification in the JWT Authorization Grant flow. By forging an assertion signed with the Identity Provider's public key (used as an HMAC secret), the attacker can obtain unauthorized access tokens and impersonate any federated user linked to the affected Identity Provider, leading to unauthorized access and privilege escalation.

Impact Parity

  • Disclosed/claimed maximum impact: Authentication bypass via JWT algorithm confusion; attacker can create unauthorized access tokens and impersonate any federated user, leading to unauthorized access and potential privilege escalation.
  • Reproduced impact from this run: Full end-to-end exploitation demonstrated. A forged HS256 JWT assertion signed with the RSA public key DER bytes as the HMAC secret was submitted to the Keycloak token endpoint. The vulnerable version (26.6.3) accepted the assertion (HTTP 200) and issued a valid Bearer access token for the federated user basic-user, issued for client test-app. The fixed version (26.6.4) rejected the same assertion with HTTP 400 invalid_grant / Invalid signature algorithm.
  • Parity: full — the claimed authentication bypass and unauthorized access token issuance were demonstrated through the real Keycloak API endpoint.

Root Cause

The vulnerability has two contributing factors in the vulnerable code:

1. Missing algorithm-type validation in AbstractBaseJWTValidator.validateSignatureAlgorithm()

The vulnerable version's validateSignatureAlgorithm(String expectedSignatureAlg) method only checks:

  • That the algorithm is not null
  • That it matches expectedSignatureAlg if that parameter is non-null

When expectedSignatureAlg is null (which occurs when the IdP config does not set jwtAuthorizationGrantAssertionSignatureAlg — the default), ANY algorithm passes validation, including symmetric algorithms like HS256 and the none algorithm. The fix adds:

  • Explicit rejection of the none algorithm
  • A check via ClientSignatureVerifierProvider.isAsymmetricAlgorithm() that rejects symmetric algorithms unless isSymmetricAlgorithmAllowed() returns true (only JWTClientSecretValidator for legitimate client-secret JWT auth allows this)
2. HMAC branch in HardcodedPublicKeyLoader

When the IdP uses useJwksUrl=false with a hardcoded key, the HardcodedPublicKeyLoader is instantiated with the alg parameter taken from the JWT header. The vulnerable version includes an HMAC branch:

} else if (JavaAlgorithm.isHMACJavaAlgorithm(algorithm)) {
    keyWrapper.setType(KeyType.OCT);
    keyWrapper.setSecretKey(KeyUtils.loadSecretKey(Base64Url.decode(encodedKey), algorithm));
}

When the JWT header specifies alg: HS256, this branch is taken. Base64Url.decode(encodedKey) decodes the publicKeySignatureVerifier config value as base64url. If the config value is the base64 content of the RSA public key (without PEM headers), this produces the exact DER-encoded SubjectPublicKeyInfo bytes — the same bytes as RSAPublicKey.getEncoded() in Java. These bytes become the HMAC secret key. The MacSignatureVerifierContext then uses this secret to verify the HS256 signature, which matches because the attacker signed with the same DER bytes.

The fix removes this HMAC branch entirely, replacing it with a warning and null key:

} else {
    logger.warnf("Unrecognized or invalid algorithm %s for hardcoded public key", algorithm);
    kw = null;
}
Attack flow
  1. The attacker obtains the Identity Provider's RSA public key (publicly available)
  2. The attacker encodes it as base64 (the raw content, without PEM headers)
  3. The attacker forges a JWT assertion with header {"alg":"HS256","typ":"JWT"} and payload containing the target federated user's subject, the IdP's issuer, and the Keycloak realm's issuer as audience
  4. The attacker signs the assertion with HMAC-SHA256 using the RSA public key's DER-encoded bytes as the secret
  5. The attacker submits the assertion to Keycloak's token endpoint with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
  6. The vulnerable Keycloak version:
    • Passes algorithm validation (no expected algorithm configured)
    • Loads the base64 public key content as an HMAC secret via HardcodedPublicKeyLoader's HMAC branch
    • Verifies the HS256 signature successfully (same bytes used for signing and verification)
    • Issues a valid access token for the impersonated federated user
Fix reference

Reproduction Steps

  1. Reference: bundle/repro/reproduction_steps.sh
  2. What the script does:
    • Pulls quay.io/keycloak/keycloak:26.6.3 (vulnerable) and quay.io/keycloak/keycloak:26.6.4 (fixed) Docker images
    • Creates a Docker network and starts both Keycloak instances in dev mode plus a Python client container
    • Installs requests and cryptography in the Python client
    • Waits for both Keycloak instances to become healthy
    • Runs a Python exploit script that, for each Keycloak instance:
      • Creates a test realm via the Admin REST API
      • Generates an RSA 2048-bit key pair
      • Creates a jwt-authorization-grant Identity Provider with the RSA public key's base64 content (no PEM headers) as publicKeySignatureVerifier, useJwksUrl=false, jwtAuthorizationGrantEnabled=true, and no jwtAuthorizationGrantAssertionSignatureAlg set
      • Creates a confidential client test-app with oauth2.jwt.authorization.grant.enabled=true and oauth2.jwt.authorization.grant.idp pointing to the IdP
      • Creates a federated user basic-user linked to the IdP with subject basic-user-id
      • Forges a JWT assertion with alg: HS256 signed with HMAC-SHA256 using the RSA public key DER bytes as the secret
      • Submits the assertion to the token endpoint with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
      • Records the HTTP response and any access token
    • Compares results: vulnerable accepts (HTTP 200 + access token), fixed rejects (HTTP 400 + "Invalid signature algorithm")
    • Captures Keycloak server logs, exploit logs, forged JWT, request/response artifacts
    • Writes runtime_manifest.json and cleans up containers
  3. Expected evidence of reproduction:
    • logs/vulnerable/response_status.txt: HTTP 200
    • logs/vulnerable/access_token.json: Contains a valid Bearer access token for basic-user issued for test-app
    • logs/fixed/response_status.txt: HTTP 400
    • logs/fixed/response_body.json: {"error":"invalid_grant","error_description":"Invalid signature algorithm"}

Evidence

Log file locations
  • bundle/logs/reproduction_steps.log — Main script log
  • bundle/logs/vulnerable/exploit.log — Exploit log for vulnerable version
  • bundle/logs/vulnerable/forged_jwt.txt — The forged HS256 JWT assertion
  • bundle/logs/vulnerable/request.txt — HTTP request details
  • bundle/logs/vulnerable/response_status.txt — HTTP response status
  • bundle/logs/vulnerable/response_body.json — Full HTTP response body
  • bundle/logs/vulnerable/access_token.json — Issued access token (decoded)
  • bundle/logs/fixed/exploit.log — Exploit log for fixed version
  • bundle/logs/fixed/response_status.txt — HTTP response status
  • bundle/logs/fixed/response_body.json — Full HTTP response body
  • bundle/logs/vuln_keycloak.log — Vulnerable Keycloak server logs
  • bundle/logs/fixed_keycloak.log — Fixed Keycloak server logs
  • bundle/repro/runtime_manifest.json — Runtime evidence manifest
Key excerpts

Vulnerable version (26.6.3) — HS256 assertion ACCEPTED:

Forged JWT header: {"alg": "HS256", "typ": "JWT"}
Forged JWT payload: {"jti": "...", "iss": "https://authorization-grant-issuer", "sub": "basic-user-id", "aud": "http://localhost:8080/realms/test-realm", "exp": ..., "iat": ...}
HMAC secret = RSA public key DER bytes (294 bytes)
Response status: 200
*** vulnerable: HS256 assertion ACCEPTED! Access token received. ***
Token preferred_username: basic-user
Token azp (client): test-app

Fixed version (26.6.4) — HS256 assertion REJECTED:

Response status: 400
fixed: HS256 assertion REJECTED. Error: invalid_grant, Description: Invalid signature algorithm
Environment details
  • Keycloak vulnerable: quay.io/keycloak/keycloak:26.6.3 (Quarkus 3.33.2, JVM 21)
  • Keycloak fixed: quay.io/keycloak/keycloak:26.6.4
  • Docker network: custom bridge network
  • Python client: python:3.12-slim with requests and cryptography
  • RSA key: 2048-bit, generated per-run
  • JWT signing: HMAC-SHA256 with RSA public key DER bytes (294 bytes) as secret

Recommendations / Next Steps

  1. Upgrade to Keycloak 26.6.4 or later — The fix adds algorithm-type validation and removes the HMAC branch from HardcodedPublicKeyLoader.
  2. Set jwtAuthorizationGrantAssertionSignatureAlg explicitly on all JWT Authorization Grant Identity Providers to pin the expected algorithm (e.g., RS256), which provides defense-in-depth even on older versions.
  3. Use JWKS URL instead of hardcoded keys where possible — the JWKS-based key loading path does not have the HMAC branch vulnerability.
  4. Audit Identity Provider configurations — check if any IdPs have publicKeySignatureVerifier set to base64 content (without PEM headers), which would make them exploitable on vulnerable versions.
  5. Revoke and rotate tokens — any access tokens issued via the JWT Authorization Grant flow on vulnerable versions should be considered potentially forged and revoked.

Additional Notes

  • Idempotency: The script is fully idempotent. It cleans up previous containers at the start and end. Each run creates fresh containers, realm, IdP, client, and user. The realm is deleted before creation if it exists.
  • Reproducibility: Verified by running the script twice consecutively — both runs produced identical results (vulnerable accepts, fixed rejects).
  • Key insight: The exploit requires the publicKeySignatureVerifier to be base64 content (not PEM with headers) because Base64Url.decode() cannot handle PEM headers (the spaces in -----BEGIN PUBLIC KEY----- cause Illegal base64 character 20). When the value is raw base64, Base64Url.decode() produces the exact DER bytes of the RSA public key, which become the HMAC secret. This is a realistic configuration scenario — an administrator may paste the base64 content of a public key without PEM headers, or import it from a source that provides base64 rather than PEM.
  • Docker networking: This environment uses Docker-in-Docker where port mapping to the host does not work. The script uses a custom Docker network with container-to-container communication via container names.

Variant Analysis & Alternative Triggers for CVE-2026-11800

Versions: versions (as tested): Red Hat Build of Keycloak / upstream Keycloak

A distinct alternate trigger (not a bypass) for CVE-2026-11800 was found and reproduced end-to-end. The original CVE is JWT algorithm confusion in the JWT Authorization Grant flow (grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer + assertion), gated by AbstractBaseJWTValidator.validateSignatureAlgorithm() (the fix's Layer 1) and HardcodedPublicKeyLoader (Layer 2). The variant reaches the same root cause / sinkHardcodedPublicKeyLoader's HMAC branch on a hardcoded RSA public key — from a different entry point that the fix's Layer 1 does not cover: the OIDC external-internal token exchange (RFC 8693, grant_type=urn:ietf:params:oauth:grant-type:token-exchange with subject_token_type=urn:ietf:params:oauth:token-type:jwt and subject_issuer=<oidc-idp>). An attacker who knows an OIDC Identity Provider's RSA public key (inherently public) forges an HS256 subject_token signed with the RSA public key DER bytes as the HMAC secret and exchanges it for a Keycloak access token for any federated user linked to that IdP. Tested against quay.io/keycloak/keycloak:26.6.3 (vulnerable — HTTP 200, access token issued for the impersonated federated user basic-user) and quay.io/keycloak/keycloak:26.6.4 (fixed — HTTP 400 invalid_token). The variant is covered by the fix's Layer 2 only (the HardcodedPublicKeyLoader HMAC-branch removal); the fix's primary algorithm-type gate (Layer 1) is never invoked on this path. This is therefore an alternate trigger, not a bypass: the released fix (26.6.4) rejects the forged token.

Fix Coverage / Assumptions

  • Invariant the fix relies on: attacker-controlled JWTs verified against an IdP's hardcoded public key either (a) pass through AbstractBaseJWTValidator.validateSignatureAlgorithm() which rejects symmetric/none algorithms, or (b) are verified by a key loader that can no longer produce an HMAC secret from a public-key config.
  • Paths explicitly covered (Layer 1): JWT Authorization Grant (DefaultJWTAuthorizationGrantValidator) and all JWT client authenticators (JWTClientValidator, JWTClientSecretValidator, FederatedJWTClientValidator, IDJWTAuthorizationGrantValidator) — every AbstractBaseJWTValidator subclass.
  • What the fix does NOT cover (Layer 1): OIDCIdentityProvider.verifySignature() and the external token-exchange / broker ID-token validation paths, which call PublicKeyStorageManager.getIdentityProviderKeyWrapper()HardcodedPublicKeyLoader directly with the JWT header's algorithm and perform no algorithm-type allow-list check. These paths are defended only by Layer 2 (HMAC-branch removal).

Variant / Alternate Trigger

Entry point: POST /realms/{realm}/protocol/openid-connect/token with grant_type=urn:ietf:params:oauth:grant-type:token-exchange, subject_token_type=urn:ietf:params:oauth:token-type:jwt, subject_issuer=<oidc-idp-alias>, requested_token_type=urn:ietf:params:oauth:token-type:access_token, subject_token=<forged HS256 JWT>, plus client_id/client_secret of a client authorized (via v1 FGAP token-exchange permission) to exchange to that IdP.

Code path: TokenExchangeGrantType.process()V1TokenExchangeProvider.tokenExchange()isExternalInternalTokenExchangeRequest()/exchangeExternalToken()OIDCIdentityProvider.exchangeExternal()exchangeExternalImpl()validateJwt()validateToken(subjectToken, ignoreAudience=true)verify(jws)verifySignature(jws)PublicKeyStorageManager.getIdentityProviderKeyWrapper()HardcodedPublicKeyLoader(kid, publicKeySignatureVerifier, alg=HS256) → (on vulnerable) HMAC branch: Base64Url.decode(base64PubKey) → RSA public key DER bytes → HMAC SecretKeyMacSignatureVerifierContext.verify() succeeds.

Files / functions:

  • services/.../protocol/oidc/tokenexchange/V1TokenExchangeProvider.java (tokenExchange, exchangeExternalToken)
  • services/.../broker/oidc/OIDCIdentityProvider.java (exchangeExternalImpl, validateJwt, validateToken, verify, verifySignature)
  • services/.../keys/loader/PublicKeyStorageManager.java (getIdentityProviderKeyWrapper)
  • services/.../keys/loader/HardcodedPublicKeyLoader.java (removed HMAC branch = the fix)

Other candidates considered and ruled out:

  • Federated JWT client authentication (clientAuthenticatorType=federated-jwt, CLIENT_AUTH_FEDERATED feature, default-enabled): same sink, but it goes through FederatedJWTClientValidatorAbstractBaseJWTValidator.validateSignatureAlgorithm() (Layer 1 covers it) and additionally its DefaultClientAssertionStrategy issuer/client lookup did not resolve in 26.6.3 dev mode in our test environment, so it could not be exercised end-to-end; regardless it is Layer-1-covered.

  • OIDC broker ID-token validation with a hardcoded key: same verifySignature sink, but the ID token arrives server-to-server from the upstream IdP's token endpoint (not directly attacker-controlled), so it does not cross the relevant trust boundary for an external attacker and is not a practical variant.

  • private_key_jwt client auth (ClientPublicKeyLoader): no HMAC branch ever existed, so not vulnerable to this confusion even pre-fix; and Layer 1 covers it post-fix.

  • JWKS-URL / JWKS-string key configs (OIDCIdentityProviderPublicKeyLoader): the JWKS is admin/IdP-controlled, not attacker-controlled, so an oct key cannot be injected by the attacker; HS256 against an RSA JWKS key fails to verify on both versions.

  • Package/component affected: org.keycloak:keycloak-servicesOIDCIdentityProvider (external token exchange), HardcodedPublicKeyLoader, PublicKeyStorageManager; same vulnerable sink as the original CVE.

  • Affected versions (as tested): Red Hat Build of Keycloak / upstream Keycloak 26.6.3 (vulnerable). 26.6.4 rejects (fixed via Layer 2).

  • Risk level: High (same class as the parent CVE; CVSS 8.1 base per GHSA-gqj5-2xp5-3qmp).

  • Consequences: An attacker with valid client credentials for a client authorized to exchange to a vulnerable OIDC IdP (hardcoded public key, validateSignature=true) can forge an HS256 subject_token and obtain a valid Keycloak access token for any federated user linked to that IdP — federated-user impersonation / privilege escalation, identical impact class to the JWT Authorization Grant variant.

Impact Parity

  • Disclosed/claimed maximum impact (parent CVE): impersonate any federated user linked to the affected Identity Provider; unauthorized access / privilege escalation.
  • Reproduced impact from this variant run: Full end-to-end — forged HS256 subject_token submitted via external token exchange; vulnerable 26.6.3 returned HTTP 200 with a Bearer access_token whose decoded payload is sub=<basic-user uuid>, azp=te-client, iss=http://localhost:8080/realms/test-realm, preferred_username=basic-user — i.e. a token minted for the impersonated federated user basic-user, issued for the attacker's client te-client.
  • Parity: full — same authentication-bypass / federated-user-impersonation impact as the parent CVE, demonstrated through the real Keycloak token endpoint.
  • Not demonstrated: No RCE/memory-corruption (not applicable to this class); no demonstration of post-impersonation lateral movement beyond token issuance.

Root Cause

HardcodedPublicKeyLoader (vulnerable versions) contains an HMAC branch keyed on the JWT header's attacker-controlled alg: when alg is HS256/HS384/HS512 and the IdP is configured with useJwksUrl=false + publicKeySignatureVerifier set to the base64 content of an RSA public key (no PEM headers), Base64Url.decode(encodedKey) yields the exact DER-encoded SubjectPublicKeyInfo bytes — the same bytes as RSAPublicKey.getEncoded() — which become the HMAC SecretKey. Because the RSA public key is inherently public, an attacker can sign an HS256 JWT with those bytes and the signature verifies. The OIDC external token-exchange path feeds the attacker's subject_token directly into OIDCIdentityProvider.verifySignature()HardcodedPublicKeyLoader with no algorithm-type allow-list check (Layer 1 is absent on this path), so on the vulnerable version the forged token is accepted and a federated-user access token is minted.

Fix commit: d343c7373dcc7bfeb9dc14de2309836299c81837 (https://github.com/keycloak/keycloak/pull/50374). The fix removes the HMAC branch (Layer 2) and adds the algorithm-type gate to AbstractBaseJWTValidator (Layer 1). Layer 2 closes this variant on 26.6.4 (the loader returns null for HS256 → verifySignature fails → invalid_token); Layer 1 does not run on this path.

Reproduction Steps

  1. Reference: bundle/vuln_variant/reproduction_steps.sh (driver) + bundle/vuln_variant/exploit_variant.py (embedded exploit).
  2. What the script does:
    • Pulls quay.io/keycloak/keycloak:26.6.3 (vulnerable) and :26.6.4 (fixed).
    • Starts both Keycloak instances in dev mode with --features=token-exchange,admin-fine-grained-authz:v1 --hostname=localhost --http-enabled=true (token-exchange v1 enables external-internal exchange; admin-fine-grained-authz:v1 enables the v1 FGAP token-exchange permission used to authorize the test client — a legitimate admin config, not a bypass).
    • Starts a Python client container (requests + cryptography).
    • For each Keycloak instance:
      • Creates test-realm; generates a 2048-bit RSA key pair; creates an OIDC IdP (providerId=oidc) with useJwksUrl=false, publicKeySignatureVerifier=<base64 of RSA public key DER>, validateSignature=true, disableTypeClaimCheck=true, syncMode=FORCE.
      • Creates a federated user basic-user linked to the IdP (userId=basic-user-id).
      • Creates a confidential client te-client and grants it the v1 FGAP token-exchange permission to the IdP (enable IdP management permissions → create a client policy listing te-client → bind it to the IdP token-exchange scope permission).
      • Forges an HS256 subject_token (iss=https://external-idp-issuer, sub=basic-user-id, aud=https://external-idp-issuer, typ=Bearer) signed with HMAC-SHA256 using the RSA public key DER bytes (294 bytes) as the secret.
      • POSTs the external token exchange and records the HTTP status/body and any access_token.
    • Captures Keycloak server logs, image build metadata, and writes runtime_manifest.json; cleans up containers/network.
  3. Expected evidence:
    • logs/vuln_variant/vulnerable/response_status.txt = HTTP 200; logs/vuln_variant/vulnerable/access_token.json contains a Bearer access token whose decoded payload has preferred_username=basic-user, azp=te-client.
    • logs/vuln_variant/fixed/response_status.txt = HTTP 400; logs/vuln_variant/fixed/response_body.json = {"error":"invalid_token","error_description":"invalid token"}.
    • logs/vuln_variant/verdict.txt = alternate_trigger.

Evidence

  • bundle/logs/vuln_variant/reproduction_steps.log — driver log
  • bundle/logs/vuln_variant/exploit_run.log — exploit stdout (both versions)
  • bundle/logs/vuln_variant/vulnerable/{exploit.log,forged_subject_token.txt,request.txt,response_status.txt,response_body.json,access_token.json}
  • bundle/logs/vuln_variant/fixed/{exploit.log,forged_subject_token.txt,request.txt,response_status.txt,response_body.json}
  • bundle/logs/vuln_variant/{vuln_keycloak.log,fixed_keycloak.log,image_metadata.txt}

Key excerpts (run 1):

Vulnerable (26.6.3):
  Forged HS256 subject_token payload: {"iss":"https://external-idp-issuer","sub":"basic-user-id","aud":"https://external-idp-issuer","typ":"Bearer",...}
  HMAC secret = RSA public key DER bytes (294 bytes)
  Response status: 200
  *** vulnerable: forged HS256 subject_token ACCEPTED! Access token received. ***
  Token sub: 16391078-5822-47ec-8f53-d15bf771c5f6
  Token azp (client): te-client
  Token preferred_username: basic-user

Fixed (26.6.4):
  Response status: 400
  fixed: forged HS256 subject_token REJECTED. Error: invalid_token, Description: invalid token

VERDICT: alternate_trigger

Environment: Keycloak 26.6.3 / 26.6.4 Docker images (Quarkus, JVM 21), --hostname=localhost, features token-exchange,admin-fine-grained-authz:v1; Python 3.12 client with requests + cryptography; RSA 2048-bit key generated per run; HMAC-SHA256 with RSA public key DER bytes (294) as secret. Docker-in-Docker with a custom bridge network (container-to-container via container names).

Recommendations / Next Steps

  1. Extend the algorithm-type gate to the broker verifySignature path. Add an explicit allow-list check in OIDCIdentityProvider.verifySignature() (and the shared AbstractOAuth2IdentityProvider/broker verify path) mirroring AbstractBaseJWTValidator.validateSignatureAlgorithm(): reject none and reject symmetric algorithms (HS256/HS384/HS512) unless the IdP is explicitly configured to allow symmetric verification. This makes external token exchange and broker ID-token validation two-layer-defended (Layer 1 + Layer 2) like the JWT Authorization Grant, instead of relying solely on HardcodedPublicKeyLoader not reintroducing an HMAC branch.
  2. Defense in depth: consider rejecting symmetric algorithms for any hardcoded-public-key IdP at the PublicKeyStorageManager.getIdentityProviderKeyWrapper() dispatch level, independent of the loader.
  3. Pin jwtAuthorizationGrantAssertionSignatureAlg / equivalent on IdPs and prefer JWKS URLs over hardcoded keys where possible.
  4. The fix is effective for this variant on 26.6.4 (Layer 2); the recommendation is about structural completeness / future-proofing, not an open bypass.

Additional Notes

  • Bypass vs alternate trigger: This is an alternate trigger (distinct entry point, same root cause/sink), not a bypass. The released fix (26.6.4) rejects the forged token (Layer 2). Per the stage spec, the reproduction script exits 1 (alternate trigger), not 0 (bypass).
  • Idempotency: Verified by running reproduction_steps.sh twice consecutively; both runs produced identical results (vulnerable HTTP 200 + token; fixed HTTP 400 invalid_token; verdict alternate_trigger).
  • Feature prerequisites: The variant requires token-exchange (v1) and admin-fine-grained-authz:v1 to be enabled. In the default 26.6.x image, TOKEN_EXCHANGE (v1) and ADMIN_FINE_GRAINED_AUTHZ (v1) are disabled by default (v2 FGAP is default-on and does not support token-exchange permission configuration); the script enables them explicitly. This is a deployment-config prerequisite (an admin who has turned on external token exchange for an IdP), analogous to the original CVE's oauth2.jwt.authorization.grant.enabled prerequisite — not a weakening of the fix.
  • Audience/type-claim handling: the forged subject_token sets payload typ=Bearer (one of OIDCIdentityProvider.SUPPORTED_TOKEN_TYPES) and the IdP sets disableTypeClaimCheck=true to pass the post-signature type check; these are payload controls, not signature controls, and do not affect the algorithm-confusion root cause.

CVE-2026-11800 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:00
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-11800 · REPRO-20
0:02
0:04
web search
0:05
web search
0:07
0:08
0:11
web search
0:14
0:15
web search
0:27
0:29
0:30
web search
0:31
0:44
0:44
extract_facts
no facts extracted
0:45
0:45
0:45
supportrepro
0:52
0:54
0:54
0:54
0:55
0:55
0:55
0:55
0:58
0:58
0:58
1:00

Artifacts and Evidence for CVE-2026-11800

Scripts, logs, diffs, and output captured during the reproduction.

bundle/artifact_promotion_manifest.json17.6 KB
bundle/vuln_variant/source_identity.json2.4 KB
bundle/vuln_variant/root_cause_equivalence.json2.8 KB
bundle/repro/reproduction_steps.sh17.2 KB
bundle/repro/rca_report.md12.5 KB
bundle/repro/runtime_manifest.json1.1 KB
bundle/logs/vulnerable/response_body.json1.4 KB
bundle/logs/fixed/response_body.json0.1 KB
bundle/logs/vulnerable/access_token.json2.2 KB
bundle/logs/vulnerable/forged_jwt.txt0.3 KB
bundle/repro/validation_verdict.json0.9 KB
bundle/logs/reproduction_steps.log11.6 KB
bundle/logs/vulnerable/exploit.log0.8 KB
bundle/logs/vulnerable/request.txt0.5 KB
bundle/logs/vulnerable/response_status.txt0.0 KB
bundle/logs/fixed/exploit.log0.7 KB
bundle/logs/fixed/response_status.txt0.0 KB
bundle/logs/vuln_keycloak.log2.7 KB
bundle/logs/fixed_keycloak.log3.2 KB
bundle/vuln_variant/reproduction_steps.sh9.4 KB
bundle/logs/vuln_variant/vulnerable/access_token.json2.3 KB
bundle/logs/vuln_variant/fixed/response_body.json0.1 KB
bundle/vuln_variant/variant_manifest.json6.3 KB
bundle/vuln_variant/validation_verdict.json2.9 KB
bundle/vuln_variant/rca_report.md15.2 KB
bundle/vuln_variant/patch_analysis.md8.4 KB
bundle/vuln_variant/runtime_manifest.json1.9 KB
bundle/logs/vuln_variant/reproduction_steps.log3.3 KB
bundle/logs/vuln_variant/exploit_run.log7.7 KB
bundle/logs/vuln_variant/vulnerable/exploit.log1.5 KB
bundle/logs/vuln_variant/vulnerable/forged_subject_token.txt0.3 KB
bundle/logs/vuln_variant/vulnerable/request.txt0.6 KB
bundle/logs/vuln_variant/vulnerable/response_status.txt0.0 KB
bundle/logs/vuln_variant/vulnerable/response_body.json1.6 KB
bundle/logs/vuln_variant/fixed/exploit.log1.1 KB
bundle/logs/vuln_variant/fixed/forged_subject_token.txt0.3 KB
bundle/logs/vuln_variant/fixed/request.txt0.6 KB
bundle/logs/vuln_variant/fixed/response_status.txt0.0 KB
bundle/logs/vuln_variant/vuln_keycloak.log2.9 KB
bundle/logs/vuln_variant/fixed_keycloak.log3.7 KB
bundle/logs/vuln_variant/image_metadata.txt2.9 KB
08 · How to Fix

How to Fix CVE-2026-11800

Upgrade keycloak/keycloak · github to 26.6.4 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-11800 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-11800

How does the CVE-2026-11800 JWT algorithm-confusion attack work?

An attacker who knows the Identity Provider's RSA public key (which is inherently public) forges a JWT assertion with alg: HS256, signing it using the RSA public key's DER-encoded bytes as the HMAC secret, and submits it via grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer to Keycloak's token endpoint. The vulnerable HardcodedPublicKeyLoader HMAC branch accepts this forged assertion and issues a valid access token for any federated user linked to that Identity Provider.

Which Keycloak versions are affected by CVE-2026-11800, and where is it fixed?

Keycloak versions before 26.6.4 in the 26.6.x line are affected. It is fixed in 26.6.4, which adds explicit algorithm-type validation rejecting symmetric algorithms in JWT public key validators and removes the HMAC branch from HardcodedPublicKeyLoader.

How severe is CVE-2026-11800?

It is rated high severity. An attacker with only public knowledge of the Identity Provider's RSA public key can forge tokens and impersonate any federated user, escalating privileges without needing the private key.

How can I reproduce CVE-2026-11800?

Download the verified script from this page and run it in an isolated environment against Keycloak before 26.6.4 with an Identity Provider configured with a hardcoded RSA public key. It forges an HS256-signed JWT assertion using that public key as the HMAC secret, submits it via the jwt-bearer grant, and shows Keycloak issuing a valid access token, then confirms 26.6.4 rejects it.
11 · References

References for CVE-2026-11800

Authoritative sources for CVE-2026-11800 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.