Research/keycloak-saml-redirect-param-split
0-dayNo CVEHighPublic

Keycloak 26.8.1, SAML redirect query-name split

Keycloak 26.8.1 SAML broker HTTP-Redirect verifies a different SAMLResponse than the document it parses. SAML%52esponse collapses onto SAMLResponse after Vert.x percent-decodes the query name, while signature reconstruction reads the raw key. No Keycloak account. Needs a redirect-signing IdP. Labbed. No CVE yet.

Name
Keycloak 26.8.1, SAML redirect query-name split
Type
0-day analysis
CVE
n/a
CVE Risk
high
Disclosure Status
public
Vendor
Red Hat
Affected
Keycloak 26.8.1 (24ed1dad). Quarkus 3.40.1. Image quay.io/keycloak/keycloak:26.8.1. Redirect-signing SAML broker with validateSignature true and wantAssertionsSigned absent or false.
Published
03 Nov 2026
Updated
03 Nov 2026
Tags
0-day, keycloak, saml, oauth, account-takeover, cwe-287, cwe-347, unauthenticated

The signature was honest. The login was not.

I am @abraxas_null. The proof of concept is on GitHub: abraxas/keycloak-saml-redirect-param-split (loopback client). The lab stack is lab/: Dockerfile, docker-compose.yml, run.sh, poc.py. Authorized lab only. It talks to loopback.

This is Keycloak 26.8.1 (24ed1dadf3f246a555adb6d3d4a99e9b3003fb3b). No CVE yet. CWE-287 / CWE-347 / CWE-116. 7.4 High (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N). Remote OAuth leftover. Unauthenticated to Keycloak. Needs a SAML identity-provider broker that signs HTTP-Redirect responses.

Someone with no Keycloak account can finish a company SAML login as another person. They sign in once at the upstream identity provider, keep that signed redirect, and stick a second SAML document in front of it under a percent-encoded name. Keycloak checks the honest signature and then logs them in as the name in the forged document. Metadata import turns signature validation on and leaves assertion signatures off. That is the usual broker.

This is not unauthenticated RCE. It is not present with no SAML broker. It is not issue #53354.

Why Keycloak

Identity brokers sit in front of everything else. A leftover there is a leftover on every app that trusts the realm. Keycloak 26.8.1 had already shipped a long CVE list. I wanted the sibling of a closed bug, not a refile.

The policy at keycloak.org/security has no active bounty. I hunted the tree anyway.

Pinning 26.8.1

Latest GA I pinned: 26.8.1, commit 24ed1dad, published 2026-10-13. Image quay.io/keycloak/keycloak:26.8.1. Quarkus 3.40.1. Not nightly. Not the Red Hat build of Keycloak.

The skip list is half the hunt

Fixed on this pin in the 26.8.1 notes. I did not refile them.

  • CVE-2026-15943, CVE-2026-18569, CVE-2026-82968
  • CVE-2026-94000 (group-join composite walk), CVE-2026-94001
  • CVE-2026-94213, CVE-2026-94215, CVE-2026-94217, CVE-2026-94218
  • CVE-2026-96445, CVE-2026-96446 (PAR object consumed before the code is stored)
  • CVE-2026-97177, CVE-2026-95503, CVE-2026-97311, CVE-2026-97846
  • CVE-2026-101333, CVE-2026-103884, CVE-2026-107121
  • Weakness issues including #53354 (semicolon as a second SAMLResponse), #49009, #49786, #51312, #51887, #52063, #52680, #52982, #53079, #53116, #53417, #53430, #53693
  • Earlier published advisories through 2026-09-16, including CVE-2026-9705 on registration-token paths

A different caller that still reaches a new sink can be a leftover. This one is.

I also dropped a same-user SSO cookie path (ForceAuthn cleared by login-actions/restart?skip_logout=true). The caller already holds that session. The assertion is for that same user. Out.

Two query maps, one name

SAMLEndpoint.redirectBinding takes @QueryParam("SAMLResponse"). RESTEasy Reactive 3.40.1 injects that through Vert.x. Netty QueryStringDecoder percent-decodes query names. SAML%52esponse (%52 is R) and SAMLResponse collapse. The first value is the forged document.

handleSamlResponse inflates that string and then calls verifyRedirectSignature.

SamlProtocolUtils.verifyRedirectSignature rebuilds the signed string from KeycloakUriInfo.getQueryParameters(false). Decode off. Raw query. SAML%52esponse stays a different key. The reconstructed string is the IdP's SAMLResponse, RelayState, and SigAlg. The signature matches.

RedirectBinding.isMessageFullySigned is true whenever those raw SigAlg and Signature parameters exist. handleLoginResponse then skips the assertion-signature requirement because wantAssertionsSigned is false and the query is treated as a full signature. callback.authenticated opens the session as the NameID in the forged assertion.

Metadata import is how most people add a SAML IdP. A signing certificate sets validateSignature and never sets wantAssertionsSigned. Boolean.valueOf of a missing key is false. Redirect is the response binding when the metadata SSO endpoint is HTTP-Redirect. The GET ACS stays mounted even when the IdP prefers POST.

The attacker sets InResponseTo to the request id from the AuthnRequest they started, copies the broker ACS into Destination, copies the realm entity id into Audience, and puts the victim NameID in the subject. RelayState stays the signed value, so it still resolves their auth session.

quarkus.http.use-semicolon-as-query-param-delimiter=false only stops ; from being a separator. It does not decode names. That is why #53354 is a different split.

The same name split exists on the IdP redirect binding. That side is not a second leftover here: Keycloak sends the assertion to the client's registered ACS.

What an attacker can do

Needs the victim NameID (usually an email) and one real signed HTTP-Redirect from that IdP. Does not need a Keycloak password. Does not need a mailbox. A POST-only IdP does not hand over that signature.

Privilege Method / sink Recon / integrity / availability Lab-proven or inferred
Bind a forged SAML document while the honest signature still verifies GET broker ACS with SAML%52esponse=<forged> ahead of signed SAMLResponse Integrity (authn) Lab-proven. Control email attacker.kcs04b8n1@lab.test. Attack email victim.kcs04b8n1@lab.test
Create the victim user through first-broker-login callback.authenticated on the forged NameID Integrity (account create) Lab-proven. Victim count 0 before the split query, 1 after
Session as an existing brokered user, including an admin who uses the same IdP Same ACS, NameID of that user Integrity (account takeover) Inferred. Lab created the victim on first login
Keep RelayState and the auth session RelayState stays the signed value Integrity Source. Signature covers RelayState
Negative control Honest signed redirect, no extra parameter n/a Lab-proven. Control finishes as the attacker email

Stays closed on this leftover

Axis Method / sink Why it is closed
Integrity Semicolon split SAMLResponse=...;SAMLResponse=... issue #53354
Integrity Unsigned assertion when wantAssertionsSigned=true handleLoginResponse then requires an assertion signature
Integrity No SAML broker, or POST-only IdP with no redirect signature Attacker never obtains SigAlg + Signature on a GET
Integrity Host RCE, unauthenticated Admin REST Out of class
Integrity Default VERIFY_PROFILE name form Lab-proven as a completion step. The form already holds the brokered email

The lab

I stood up stock quay.io/keycloak/keycloak:26.8.1 on 127.0.0.1:18084. start-dev. The lab IdP is in-process. It never talks to a real identity provider. validateSignature=true. No wantAssertionsSigned key. Image label and serverinfo are both 26.8.1.

The first urllib client followed 302s and dropped AUTH_SESSION_ID from the first hop. Symptom: HTTP 400 "Restart login cookie not found." I stopped the redirect so the Set-Cookie survives. That is a lab-client fact, not the leftover.

Default VERIFY_PROFILE still asks for a first and last name. The form already contains the brokered email. Submitting those two name fields does not change the email. I treated that as a completion step, not a miss.

Control token email attacker.kcs04b8n1@lab.test. Victim user count 0. Attack token email victim.kcs04b8n1@lab.test. Victim after = 1.

Plain text
IOC image=quay.io/keycloak/keycloak:26.8.1 label-version=26.8.1
IOC serverinfo-version=26.8.1
IOC validate-signature=true want-assertions-signed=absent
IOC control-email=attacker.kcs04b8n1@lab.test
IOC victim-before=0
IOC attack-email=victim.kcs04b8n1@lab.test
IOC victim-after=1
SUCCESS KC-H04-SAML-REDIRECT kcs04b8n1

Wrong turns already recorded: treating #53354 as this leftover; following ACS 302s in the lab client; reading empty firstName/lastName as a miss; a reverse shell. Theatre.

The fix

Verify the same query document the endpoint parses. Percent-decode parameter names before signature reconstruction, or reject a query that contains more than one SAMLResponse after decoding. Turn wantAssertionsSigned on for redirect brokers, including the metadata-import path.

Until then, a redirect-signing SAML IdP with assertion signatures off is an account-takeover broker. One login at that IdP harvests the signature.

I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.

Keycloak 26.8.1, SAML redirect query-name split · Abraxas Labs