Keycloak SAML Auth Bypass, Fixed in 26.8.2 (CVE-2026-117691)
Keycloak's SAML redirect binding verifies a different parameter than it processes, letting an attacker log in as any federated user; fixed in 26.8.2.
TL;DR
- A flaw in Keycloak's SAML HTTP-Redirect binding lets an attacker log in as any federated user by sending an unsigned message next to a signed one. Red Hat published CVE-2026-117691 on 2026-11-05.
- Realms that broker an external SAML identity provider are exposed when signature validation is on and assertions are not required to be signed. Red Hat Build of Keycloak is affected; Red Hat Single Sign-On 7 is not.
- Red Hat, the CVE Numbering Authority, scores it CVSS v3.1 9.1 and rates its own products' impact Important. No public exploit or entry in the Cybersecurity and Infrastructure Security Agency (CISA) Known Exploited Vulnerabilities (KEV) catalog existed as of 2026-11-05.
- Upgrade to Keycloak 26.8.2, released on 2026-11-05. Red Hat had published no errata when checked; see "How do I fix it?" for the workarounds and their limits.
Am I affected?
You are affected if your realm brokers an external SAML identity provider over the HTTP-Redirect binding and runs a Keycloak release that carries the vulnerable check.
- The realm has a SAML identity provider configured for identity brokering, and that provider returns its responses over the HTTP-Redirect binding.
- Signature validation is on for that identity provider. Keycloak turns it on when it imports the provider's signing certificate from metadata, and the bypass only matters when Keycloak actually verifies a signature.
- The identity provider does not require signed assertions. With "Want Assertions Signed" on, Keycloak rejects the unsigned crafted assertion.
- The Keycloak version is affected according to the version table.
Red Hat lists Red Hat Single Sign-On 7 as not affected because the vulnerable code is not present. Red Hat Build of Keycloak is affected, and Red Hat had published no errata when checked on 2026-11-05. The main branch (nightly) of keycloak/keycloak still carried the old check on that date; 26.8.2 is the fixed release.
| Branch | Affected versions | Fixed version |
|---|---|---|
| Keycloak 26.8 | 26.8.0 to 26.8.1 | 26.8.2 (released 2026-11-05) |
| Keycloak 26.6 | 26.6.0 to 26.6.7 | 26.6.8 (not published on 2026-11-05) |
| Keycloak 26.4 | 26.4.0 to 26.4.16 | 26.4.17 (not published on 2026-11-05) |
| Keycloak 26.7, 26.5 and older | carry the same check; no fixed release announced | move to 26.8.2 |
| Red Hat Build of Keycloak | affected; Red Hat lists no fixed version | no errata as of 2026-11-05 |
| Red Hat Single Sign-On 7 | not affected | not applicable |
Red Hat, as the CVE Numbering Authority, scores the flaw CVSS v3.1 9.1. The vector is CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N. The base severity is Critical, while Red Hat's own product impact rating is Important. NVD had not analyzed the record when checked on 2026-11-05.
Keycloak maintains three release branches. One has a published fix, and the other two have fixes labelled for 26.6.8 and 26.4.17 but not published on 2026-11-05.
Check what is running, on the host and inside every container:
/opt/keycloak/bin/kc.sh --version
docker ps --format '{{.Image}}' | grep -i keycloak
docker exec <container-name> /opt/keycloak/bin/kc.sh --version
kubectl get deploy -A -o jsonpath='{..image}' | tr ' ' '\n' | grep -i keycloak
Red Hat Build of Keycloak uses the same kc.sh binary inside its image, so the docker exec check applies to registry.redhat.io/rhbk/keycloak-rhel9 as well.
How do I fix it?
Upgrade to Keycloak 26.8.2, the first upstream release with the fix, or to the backport for your branch once it is published. For a container deployment, pull the fixed image first:
docker pull quay.io/keycloak/keycloak:26.8.2
- Recreate the container. A running container keeps the old image until it is recreated.
- For a ZIP installation, download
keycloak-26.8.2.zipfrom the release page. Back up the configuration and the database, then replace the installation. - For Red Hat Build of Keycloak, Red Hat had published no errata on 2026-11-05. Track the Red Hat advisory and update
rhbk/keycloak-rhel9when a fixed build appears. - Red Hat Single Sign-On 7 needs no action for this CVE.
Red Hat lists no mitigation that meets its product security criteria. If you cannot upgrade, two options reduce exposure. Neither is a supported fix.
- Require signed assertions: turn on "Want Assertions Signed" for the identity provider when that provider signs assertions. Keycloak then rejects the unsigned crafted assertion. This covers CVE-2026-117691 for that provider, but it changes the trust contract and does nothing if the provider does not sign assertions.
- Reject ambiguous parameters at the edge. On the reverse proxy in front of Keycloak, reject requests to the broker endpoint that contain a percent-encoded parameter name. Also reject a request that repeats one of the signed parameters:
SAMLResponseSAMLRequestRelayStateSigAlgSignature
This mirrors the fix, but the proxy must decode names before matching and the rule can break clients that send unusual encodings. Run it in log-only mode first.
What is the root cause?
The bug is a mismatch between the parameter name Keycloak uses to process a SAML message and the name it uses to verify that message's signature.
Keycloak's SAML broker accepts an authentication response from an external identity provider through the HTTP-Redirect binding. In that binding the whole SAML message travels in a query parameter, and the identity provider signs the query string rather than the XML document.
The broker entry point is SAMLEndpoint.redirectBinding. It declares samlResponse as a JAX-RS @QueryParam, so the framework decodes parameter names before it matches them. When a request carries both %53AMLResponse and SAMLResponse, both decode to SAMLResponse, and the method receives the first value in query order.
// services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java, 26.8.1, lines 182-194
@GET
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
@QueryParam(GeneralConstants.SAML_ARTIFACT_KEY) String samlArt,
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
if (Objects.isNull(samlArt)) {
return new RedirectBinding().execute(samlRequest, samlResponse, null, relayState, null);
}
return new ArtifactBinding().execute(samlRequest, samlResponse, samlArt, relayState, null);
}
The verifier reads the query through a different API. KeycloakUriInfo.getQueryParameters(false) is meant to return the raw query. In 26.8.1 it calls parseQueryParameters(rawQuery, false), and that false disables decoding for names as well as values.
// server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java, 26.8.1, lines 147-165
public MultivaluedMap<String, String> getQueryParameters(boolean decode) {
if (decode) {
return delegate.getQueryParameters(decode);
}
MultivaluedMap<String, String> result = new MultivaluedHashMap<>();
String rawQuery = delegate.getRequestUri().getRawQuery();
if (rawQuery == null) {
return result;
}
for (Map.Entry<String, List<String>> entry : parseQueryParameters(rawQuery, false).entrySet()) {
result.put(entry.getKey(), entry.getValue());
}
return result;
}
parseQueryParameters splits the raw query on & and decodes a name only when its decode argument is true. With false, %53AMLResponse stays %53AMLResponse.
// common/src/main/java/org/keycloak/common/util/UriUtils.java, 26.8.1, lines 61-90
String[] nv = param.split("=", 2);
try {
String name = decode ? URLDecoder.decode(nv[0], "UTF-8") : nv[0];
String val = nv.length > 1 ? nv[1] : "";
map.add(name, decode ? URLDecoder.decode(val, "UTF-8") : val);
}
SamlProtocolUtils.verifyRedirectSignature looks up paramKey, which is SAMLResponse, in that raw map. The percent-encoded copy is invisible under that key, so getFirst returns the literal, signed message. The method rebuilds the signed string from that value, RelayState and SigAlg, and verifies the identity provider's signature over it.
// services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java, 26.8.1, lines 216-251
public static void verifyRedirectSignature(SAMLDocumentHolder documentHolder, KeyLocator locator, UriInfo uriInformation, String paramKey) throws VerificationException {
MultivaluedMap<String, String> encodedParams = uriInformation.getQueryParameters(false);
verifyRedirectSignature(documentHolder, locator, encodedParams, paramKey);
}
public static void verifyRedirectSignature(SAMLDocumentHolder documentHolder, KeyLocator locator, MultivaluedMap<String, String> encodedParams, String paramKey) throws VerificationException {
String request = encodedParams.getFirst(paramKey);
String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
String signature = encodedParams.getFirst(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
String relayState = encodedParams.getFirst(GeneralConstants.RELAY_STATE);
if (request == null) throw new VerificationException("SAM was null");
if (algorithm == null) throw new VerificationException("SigAlg was null");
if (signature == null) throw new VerificationException("Signature was null");
String keyId = getMessageSigningKeyId(documentHolder.getSamlObject());
StringBuilder rawQueryBuilder = new StringBuilder().append(paramKey).append("=").append(request);
if (encodedParams.containsKey(GeneralConstants.RELAY_STATE)) {
rawQueryBuilder.append("&" + GeneralConstants.RELAY_STATE + "=").append(relayState);
}
rawQueryBuilder.append("&" + GeneralConstants.SAML_SIG_ALG_REQUEST_KEY + "=").append(algorithm);
String rawQuery = rawQueryBuilder.toString();
try {
byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature);
String decodedAlgorithm = RedirectBindingUtil.urlDecode(encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY));
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm);
if (!RedirectBindingSignatureUtil.validateRedirectBindingSignature(signatureAlgorithm,
rawQuery.getBytes(StandardCharsets.UTF_8), decodedSignature, locator, keyId)) {
throw new VerificationException("Invalid query param signature");
}
} catch (Exception e) {
throw new VerificationException(e);
}
}
The signature is valid, but it covers a message the server will not process. redirectBinding has already passed the crafted first value to execute, and handleLoginResponse later treats the message as fully signed because SigAlg and Signature are present. The crafted assertion itself is never signed.
// services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java, 26.8.1, lines 614-620
boolean signed = AssertionUtil.isSignedElement(assertionElement);
final boolean assertionSignatureNotExistsWhenRequired = config.isWantAssertionsSigned() && !signed;
final boolean signatureNotValid = signed && config.isValidateSignature() && !AssertionUtil.isSignatureValid(assertionElement, getIDPKeyLocator());
final boolean hasNoSignatureWhenRequired = !signed && config.isValidateSignature() && !isMessageFullySigned(holder);
For the redirect binding, isMessageFullySigned returns containsUnencryptedSignature, which only checks that SigAlg and Signature are present in the query.
// services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java, 26.8.1, lines 898-916
@Override
protected boolean isMessageFullySigned(SAMLDocumentHolder documentHolder) {
return containsUnencryptedSignature(documentHolder);
}
@Override
protected boolean containsUnencryptedSignature(SAMLDocumentHolder documentHolder) {
MultivaluedMap<String, String> encodedParams = session.getContext().getUri().getQueryParameters(false);
String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY);
String signature = encodedParams.getFirst(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY);
return algorithm != null && signature != null;
}
The verifier and the processor disagree about which query parameter is SAMLResponse, so the signature validates one message while the broker parses another.
How is it exploited?
The attacker needs one validly signed redirect-binding message from the identity provider and a login session they started. The attacker then sends both messages in the order that splits verification from processing.
- The attacker has an account at the external SAML identity provider that the Keycloak realm brokers.
- The attacker starts a broker login and captures one validly signed redirect-binding message that the identity provider issues for that account. Red Hat's description gives a logout response as an example.
- The attacker builds an unsigned SAML response that asserts the victim's identity, with the
DestinationandInResponseToof the login session they started. - The attacker sends a GET request to the broker endpoint. The crafted response comes first, under
%53AMLResponse. The legitimate signed message comes second, under literalSAMLResponse. The request keeps the signed message'sRelayState,SigAlgandSignature. - JAX-RS decodes both parameter names to
SAMLResponseand passes the first value, the crafted response, toexecute. verifyRedirectSignaturereads the raw query without decoding names, finds only the literalSAMLResponse, and validates the signature over the signed message.- The crafted response is parsed, the unsigned assertion passes the "message fully signed" check because the query carries
SigAlgandSignature, and Keycloak logs the attacker in as the victim.
No public proof of concept was known when checked on 2026-11-05. These steps come from Red Hat's Bugzilla description and the code paths in "What is the root cause?".
How does the attack flow?
The attacker pairs an unsigned assertion with a signed message so that Keycloak verifies the signed one but processes the unsigned one.
starts a broker login"] --> B["Captures a signed
redirect-binding message"] --> C["Sends the crafted message first,
the signed SAMLResponse second"] --> D["Server verifies the signed message
but processes the crafted one"] --> E["No check that the signed
value is the processed one"] --> F["Login as the victim"]
What can an attacker do?
An attacker who completes the bypass logs in as the federated user named in the crafted assertion, with that user's roles and data.
The bypass needs all of these conditions:
- An account at the external identity provider, which the attacker uses to obtain one validly signed redirect-binding message.
- A broker login session that the attacker started, so the crafted response can carry the matching
DestinationandInResponseTo. - A realm that brokers that identity provider over the HTTP-Redirect binding.
- Signature validation on and "Want Assertions Signed" off, the common configuration.
- Network access to the Keycloak broker endpoint.
Red Hat's statement says successful exploitation lets an attacker "log in as any user and access their roles and data without authorization". The impact follows the victim's roles, so a victim with administrative access in a connected application carries that access. The CVSS vector sets availability impact to None, and the advisory does not describe code execution or a denial of service.
Why does the fix work?
The fix does three things. It makes the signature check use the same decoded parameter names as the processing path. It rejects duplicate SAML parameters. It compares the signed value with the value being processed.
The shared URI parser now decodes parameter names while leaving values encoded:
--- a/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java
+++ b/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java
@@
- for (Map.Entry<String, List<String>> entry : parseQueryParameters(rawQuery, false).entrySet()) {
+ for (Map.Entry<String, List<String>> entry : parseQueryParameters(rawQuery, true, false).entrySet()) {
With names decoded, %53AMLResponse and SAMLResponse both appear under SAMLResponse in the raw view, and the new duplicate check rejects the request:
--- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java
@@
+ public static void rejectRepeatedRedirectParameters(MultivaluedMap<String, String> encodedParams) throws VerificationException {
+ for (String name : REDIRECT_SIGNATURE_PARAMETERS) {
+ List<String> values = encodedParams.get(name);
+ if (values != null && values.size() > 1) {
+ throw new VerificationException("Query parameter " + name + " is present more than once");
+ }
+ }
+ }
+
+ private static void verifySignedValueIsProcessed(MultivaluedMap<String, String> encodedParams, String paramKey, String processedValue) throws VerificationException {
+ String signedValue = encodedParams.getFirst(paramKey);
+ String decodedSignedValue;
+ try {
+ decodedSignedValue = signedValue == null ? null : RedirectBindingUtil.urlDecode(signedValue);
+ } catch (IOException | IllegalArgumentException e) {
+ throw new VerificationException("Invalid encoding of query parameter " + paramKey, e);
+ }
+ if (!Objects.equals(decodedSignedValue, processedValue)) {
+ throw new VerificationException("Query parameter " + paramKey + " being processed is not the signed one");
+ }
+ }
The endpoint now passes the message it is processing into the verifier:
--- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@
- verifySignature(GeneralConstants.SAML_RESPONSE_KEY, holder);
+ verifySignature(GeneralConstants.SAML_RESPONSE_KEY, holder, samlResponse, relayState);
verifySignedValueIsProcessed decodes the signed value and compares it with the value passed to execute. A signed message that differs from the processed one fails even if the duplicate check is bypassed. RelayState gets the same comparison. rejectRepeatedRedirectParameters is also called from basicChecks and from the redirect binding in SamlService, so both redirect endpoints reject duplicates before processing.
The commit fixes both copies of the redirect-binding check it touches: the broker endpoint in SAMLEndpoint and the identity-provider endpoint in SamlService. The POST and artifact bindings do not read the query string this way and are unchanged.
How do I check if I was compromised?
No public indicators of compromise have been published for CVE-2026-117691 as of 2026-11-05.
The public description gives one lead: a percent-encoded SAML parameter name in a request to the broker endpoint. If your reverse proxy keeps query strings, search its access log:
grep -iE '[?&](%[0-9a-f]{2})+(amlresponse|amlrequest|elaystate|igalg|ignature)=' /var/log/nginx/access.log
If your logs store decoded query strings, look for two SAMLResponse parameters in one request instead:
grep -iE '[?&]samlresponse=[^&]*&.*[?&]samlresponse=' /var/log/nginx/access.log
A hit is a lead, not proof: a legitimate client could percent-encode a name or send a duplicate parameter. Confirm it against the full request and the identity provider's own logs. A failed attempt may leave no trace if query strings are not logged, so the absence of a hit does not prove that no attempt was made.
Timeline
- 2026-10-26: Reported to Red Hat.
- 2026-11-05: CVE-2026-117691 published.
- 2026-11-05: Fix commit
cbda2bfd35lands in keycloak/keycloak. - 2026-11-05: Keycloak 26.8.2 released.
- 2026-11-05: No public proof of concept and no CISA KEV entry when checked.
Ten days passed between the report and the public disclosure. The fix commit landed 1 hour 52 minutes after the CVE record was published, and the 26.8.2 release followed 41 minutes later.
FAQ
Does this affect Keycloak when it acts as a SAML identity provider rather than a broker?
The account-takeover impact is on the broker path, where Keycloak consumes a response from an external identity provider. The same commit also hardens the redirect-binding check in SamlService, the identity-provider endpoint, so the fix covers both paths.
Does the attacker need a Keycloak account?
No. Red Hat scores the attack with no privileges required because no Keycloak account is needed. The attacker does need an account at the external identity provider to obtain one validly signed redirect-binding message.
Is "Want Assertions Signed" a fix?
It blocks the unsigned crafted assertion, so it reduces exposure. It is not a substitute for 26.8.2 because it changes the identity provider contract and only works when that provider signs assertions.
Is there a public exploit?
No public proof of concept or exploitation in the wild was known as of 2026-11-05. CISA's Authorized Data Publisher (ADP) record lists Exploitation: none, and the CVE is not in the KEV catalog.
Which release fixes it?
Keycloak 26.8.2, released on 2026-11-05. The 26.6.8 and 26.4.17 backports were labelled but not published when checked, and Red Hat Build of Keycloak had no errata.
Is Red Hat Single Sign-On 7 affected?
No. Red Hat lists it as not affected because the vulnerable code is not present.
References
- Red Hat, "CVE-2026-117691" advisory, https://access.redhat.com/security/cve/CVE-2026-117691 (checked November 5, 2026).
- CVE Program, "CVE-2026-117691" record, https://www.cve.org/CVERecord?id=CVE-2026-117691 (checked November 5, 2026).
- Red Hat Bugzilla, "CVE-2026-117691 keycloak-services: SAML redirect binding signature bypass via percent-encoded parameter names", https://bugzilla.redhat.com/show_bug.cgi?id=2561977 (checked November 5, 2026).
- keycloak/keycloak, "Verify the SAML redirect binding message that is processed", commit
cbda2bfd35e2eb182942996832b819c35780b121, https://github.com/keycloak/keycloak/commit/cbda2bfd35e2eb182942996832b819c35780b121 (checked November 5, 2026). - keycloak/keycloak, "26.8.2" release, https://github.com/keycloak/keycloak/releases/tag/26.8.2 (checked November 5, 2026).
- keycloak/keycloak,
SamlProtocolUtils.javaat 26.8.1, https://github.com/keycloak/keycloak/blob/26.8.1/services/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java (checked November 5, 2026). - keycloak/keycloak,
SAMLEndpoint.javaat 26.8.1, https://github.com/keycloak/keycloak/blob/26.8.1/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java (checked November 5, 2026). - keycloak/keycloak,
KeycloakUriInfo.javaat 26.8.1, https://github.com/keycloak/keycloak/blob/26.8.1/server-spi/src/main/java/org/keycloak/models/KeycloakUriInfo.java (checked November 5, 2026). - keycloak/keycloak,
UriUtils.javaat 26.8.1, https://github.com/keycloak/keycloak/blob/26.8.1/common/src/main/java/org/keycloak/common/util/UriUtils.java (checked November 5, 2026). - keycloak/keycloak, "CVE-2026-117691 SAML redirect binding signature bypass via percent-encoded parameter name", issue 54714, https://github.com/keycloak/keycloak/issues/54714 (checked November 5, 2026).
- Keycloak, "Configuring a SAML identity provider", server administration guide, https://www.keycloak.org/docs/26.8.1/server_admin/#_identity_broker_saml (checked November 5, 2026).
- Cybersecurity and Infrastructure Security Agency, Known Exploited Vulnerabilities catalog, https://www.cisa.gov/known-exploited-vulnerabilities-catalog (checked November 5, 2026).
- National Vulnerability Database, "CVE-2026-117691", https://nvd.nist.gov/vuln/detail/CVE-2026-117691 (checked November 5, 2026).