# REPRO-2026-00243: Keycloak JWT algorithm confusion privilege escalation ## Summary Status: published Severity: high Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00243 CVE: CVE-2026-11800 ## Package Name: keycloak/keycloak Ecosystem: github Affected: Keycloak < 26.6.4 (26.6.x line) Fixed: 26.6.4 ## Root Cause # RCA Report: CVE-2026-11800 — Keycloak JWT Algorithm Confusion Privilege Escalation ## Summary 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`. ## Impact - **Package/component affected:** `org.keycloak:keycloak-services` — `AbstractBaseJWTValidator`, `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: ```java } 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: ```java } 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 - PR: https://github.com/keycloak/keycloak/pull/50374 - Commit: `d343c7373dcc7bfeb9dc14de2309836299c81837` - Title: "Remove support for symmetric algorithms in JWT public key validators" ## 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. ## Reproduction Details Reproduced: 2026-07-06T08:33:34.482Z Duration: 2760 seconds Tool calls: 281 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00243 pruva-verify CVE-2026-11800 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00243&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00243/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-11800 - Source: https://github.com/keycloak/keycloak ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 17635 bytes) - bundle/repro/rca_report.md (analysis, 12830 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 9656 bytes) - bundle/vuln_variant/rca_report.md (analysis, 15613 bytes) - bundle/artifact_promotion_manifest.json (other, 18001 bytes) - bundle/vuln_variant/source_identity.json (other, 2428 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 2900 bytes) - bundle/repro/runtime_manifest.json (other, 1096 bytes) - bundle/logs/vulnerable/response_body.json (other, 1453 bytes) - bundle/logs/fixed/response_body.json (other, 84 bytes) - bundle/logs/vulnerable/access_token.json (other, 2222 bytes) - bundle/logs/vulnerable/forged_jwt.txt (other, 339 bytes) - bundle/repro/validation_verdict.json (other, 875 bytes) - bundle/logs/reproduction_steps.log (log, 11876 bytes) - bundle/logs/vulnerable/exploit.log (log, 837 bytes) - bundle/logs/vulnerable/request.txt (other, 538 bytes) - bundle/logs/vulnerable/response_status.txt (other, 9 bytes) - bundle/logs/fixed/exploit.log (log, 742 bytes) - bundle/logs/fixed/response_status.txt (other, 9 bytes) - bundle/logs/vuln_keycloak.log (log, 2724 bytes) - bundle/logs/fixed_keycloak.log (log, 3281 bytes) - bundle/logs/vuln_variant/vulnerable/access_token.json (other, 2307 bytes) - bundle/logs/vuln_variant/fixed/response_body.json (other, 70 bytes) - bundle/vuln_variant/variant_manifest.json (other, 6420 bytes) - bundle/vuln_variant/validation_verdict.json (other, 2946 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 8599 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1981 bytes) - bundle/logs/vuln_variant/reproduction_steps.log (log, 3338 bytes) - bundle/logs/vuln_variant/exploit_run.log (log, 7872 bytes) - bundle/logs/vuln_variant/vulnerable/exploit.log (log, 1509 bytes) - bundle/logs/vuln_variant/vulnerable/forged_subject_token.txt (other, 333 bytes) - bundle/logs/vuln_variant/vulnerable/request.txt (other, 660 bytes) - bundle/logs/vuln_variant/vulnerable/response_status.txt (other, 9 bytes) - bundle/logs/vuln_variant/vulnerable/response_body.json (other, 1617 bytes) - bundle/logs/vuln_variant/fixed/exploit.log (log, 1153 bytes) - bundle/logs/vuln_variant/fixed/forged_subject_token.txt (other, 333 bytes) - bundle/logs/vuln_variant/fixed/request.txt (other, 661 bytes) - bundle/logs/vuln_variant/fixed/response_status.txt (other, 9 bytes) - bundle/logs/vuln_variant/vuln_keycloak.log (log, 2978 bytes) - bundle/logs/vuln_variant/fixed_keycloak.log (log, 3824 bytes) - bundle/logs/vuln_variant/image_metadata.txt (other, 2928 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00243 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00243/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00243 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev