
Loopback PoC for a Keycloak 26.8.1 SAML broker flaw: a percent-encoded duplicate SAMLResponse name bypasses redirect signature verification, allowing login as another user.
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. This needs a SAML broker that signs HTTP-Redirect responses, with signature validation on and assertion signatures left off, which is what metadata import configures. It is not the semicolon split from issue #53354.
Class: OAuth leftover Reach: Remote (unauthenticated to Keycloak; needs a redirect-signing SAML broker)
Keycloak 26.8.1 (24ed1dad) - Red Hat
| ID | no CVE yet |
| Class | OAuth leftover |
| Reach | Remote (no Keycloak account; needs one genuine HTTP-Redirect signature from the IdP) |
| CWE | CWE-287 / CWE-347 / CWE-116 |
| CVSS | High: 7.4 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Product | Keycloak |
| Affected | 26.8.1 (24ed1dadf3f246a555adb6d3d4a99e9b3003fb3b); Quarkus 3.40.1; image quay.io/keycloak/keycloak:26.8.1 |
| Auth | none on Keycloak. Needs a SAML identity-provider broker with validateSignature=true and wantAssertionsSigned absent or false. |
| License | GNU Affero GPL v3.0 |
| Lab | 127.0.0.1:18084 only |
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. The GET ACS stays mounted even when metadata import prefers POST.
| 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 [email protected]. Attack email [email protected] |
| 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. quarkus.http.use-semicolon-as-query-param-delimiter=false |
| 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, not a disproof. The form already holds the brokered email |
I pinned Keycloak 26.8.1. RESTEasy Reactive takes @QueryParam("SAMLResponse") from Vert.x, which percent-decodes query names, so SAML%52esponse collapses onto SAMLResponse and the first value wins. verifyRedirectSignature rebuilds the signed string from getQueryParameters(false) on the raw query, where that name stays a different key.
Wrong turns already recorded: treating #53354 as this leftover (percent-encoded name, not semicolon); following ACS 302s in the lab client and dropping AUTH_SESSION_ID; reading empty firstName/lastName on VERIFY_PROFILE as a miss (the email is already the brokered NameID); a reverse shell. Theatre.
Python 3.11+ with cryptography. Docker required.
python3 keycloak-saml-redirect-param-split-Abraxas-Labs.py
or:
cd lab
./run.sh
Target only http://127.0.0.1:18084. Image quay.io/keycloak/keycloak:26.8.1. The launcher chdirs into lab/ and execs run.sh. The lab IdP is in-process. It never talks to a real identity provider.
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 review-profile=DISABLED
IOC [email protected]
IOC victim-before=0
IOC [email protected]
IOC victim-after=1
SUCCESS KC-H04-SAML-REDIRECT kcs04b8n1
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.
24ed1dadf3f246a555adb6d3d4a99e9b3003fb3b)SAMLEndpoint.redirectBinding (SAMLEndpoint.java:182-183, :779-813, :896-914)SamlProtocolUtils.verifyRedirectSignature (SamlProtocolUtils.java:216-249)KeycloakUriInfo.getQueryParameters (KeycloakUriInfo.java:147-163)GNU Affero GPL v3.0. See LICENSE. Loopback lab only. No warranty.
Abraxas Labs [email protected]
Please credit CVE to Abraxas Labs.