Kai Ole Hartwig
22 min read
By

Keycloak CVE-2026-117691: The signature covers a different SAML message than the one processed

In the HTTP-Redirect binding, a SAML message travels in the URL, and its signature covers a string of query parameters rather than the XML itself. Up to version 26.8.1, Keycloak read those parameters in two different ways. Signature verification saw the raw query, processing saw a version with decoded parameter names. A parameter whose name was written in percent-encoding thus stayed invisible to one side and became the actual message for the other. CVE-2026-117691 turns this into a login as any federated user, as soon as an attacker holds a single validly signed response from the identity provider. Keycloak 26.8.2 therefore unifies the parsing and also checks whether the signed value is the one being processed.

TL;DR — 90 seconds

Two readings of the same URL

When Keycloak acts as an identity broker, it sends users to an external identity provider with an AuthnRequest. What comes back is a SAML response with an assertion that records who signed in there. How that response travels is governed by the bindings. In the POST binding, the signature sits inside the document as an XML signature. In the HTTP-Redirect binding, by contrast, the message is compressed, Base64-encoded and attached to the URL as the parameter SAMLResponse, while the signature sits next to it in a separate Signature parameter. According to the SAML bindings specification, it covers the string SAMLResponse=…&RelayState=…&SigAlg=… built from the URL-encoded values, not the XML.

Why this is delicate is spelled out in the specification itself. The order of parameters in the URL is not fixed, so the receiver has to reassemble the signed string on its own. And because the same value has several valid URL encodings, it must use the original values exactly as they arrived. Every implementation therefore holds two versions of the same request: the raw one for the signature and the decoded one for processing. That works as long as both are derived from the same parameters. If two parsers split the URL differently, however, one side checks something other than what the other side uses. This pattern is called a parser differential. It is closely related to XML Signature Wrapping in the POST binding, where verifier and processor look at different elements of the same document.

This is exactly where the flaw was. As Ostorlab's analysis shows, the method SAMLEndpoint.redirectBinding receives the value through the JAX-RS annotation @QueryParam. The framework decodes parameter names before matching them, so a name written in percent-encoding also counts as SAMLResponse. If both spellings appear in the request, the method gets whichever value comes first. Signature verification in SamlProtocolUtils.verifyRedirectSignature, on the other hand, builds its string from KeycloakUriInfo.getQueryParameters(false), where names stay encoded. To it, the encoded parameter simply does not exist under the key SAMLResponse. So it checks the genuine, signed response while the broker reads a second response that nobody ever signed.

A second weakness completes the picture. The check containsUnencryptedSignature only establishes that SigAlg and Signature are present and never asks whether they belong to the processed value. The message is therefore treated as fully signed. Keycloak then no longer checks a signature at assertion level, provided the assertion is unsigned and “Want Assertions Signed” stays off.

The problem is not new at this spot. Keycloak 26.8.1 from 13 October already listed entry #53354 under weaknesses, without a CVE number: there too, the Redirect binding verified the signature over a different SAMLResponse than the one processed. Phase Two describes that variant as splitting via a semicolon used as parameter separator. Within two releases, CVE-2026-117691 is thus the second bypass in the same code.

What the fix changes

The fix is commit 0685265 on the main branch, which carries over the correction shipped with 26.8.2, 26.6.9 and 26.4.18. It touches twelve files, five of them outside the tests, and its commit message names the cause with unusual candour: the signature was verified over the raw query, but the message was read through @QueryParam, which decodes names and takes the first occurrence.

The correction works in three places. First, it aligns the two readings. UriUtils.parseQueryParameters gets separate switches for names and values, and KeycloakUriInfo.getQueryParameters(false) now decodes names while values stay raw. Encoded and literal names thus end up under the same key, and the Javadoc states explicitly that both views must agree for signature checks. Next, a new check rejects any request in which SAMLResponse, SAMLRequest, RelayState, SigAlg or Signature appears more than once, even when no signature is required. In the Redirect binding, this block already applies in the basic checks and ends with a 400 error. Finally, Keycloak decodes the signed value and requires it to match the processed one. To make this possible, SAMLEndpoint now passes the processed message and the RelayState to verifySignature. The changes cover the broker endpoint SAMLEndpoint and the IdP endpoint SamlService, while the POST and Artifact bindings do not read the query this way.

The last step reveals the most. The first two close ways in which two parsers can drift apart, and 26.8.1 had already closed one such way. The third check no longer asks how a divergence arises. It enforces an invariant: only what was signed gets processed. My reading is that the real cause was neither the semicolon nor the percent-encoding. It was a design in which verification and processing each obtained their input on their own.

From your own IdP account to someone else's identity

The attacker does not need to sign in to Keycloak, and Ostorlab and Red Hat Bugzilla agree on that. All it takes is a realm that brokers an external SAML identity provider, whose broker endpoint is reachable from the network, and whose IdP configuration has “Validate Signature” on and “Want Assertions Signed” off. Add to that an account of their own at the identity provider, through which the attacker obtains a validly signed response in the Redirect binding. They start the broker login themselves so that this response matches an ongoing session. That it is precisely the realms which check signatures that are affected follows a bitter logic. If you have switched “Validate Signature” off, the Keycloak documentation says you accept unsigned messages anyway, and a forgery needs no flaw at all.

The flow itself is quickly told. The attacker signs in to the identity provider with their own account and keeps the signed response. Next to it they place a self-built, unsigned response carrying the name of another federated user, hidden under an encoded parameter name. Keycloak checks the genuine signature, finds it valid and then signs in the user from the forged response. Afterwards the log shows nothing more than an ordinary broker login.

What this session is worth depends on the realm. The attacker takes over the victim's identity with every role Keycloak has granted them through mappers or directly, and reaches every application in the realm through single sign-on. If administrators log in through the same SAML broker, the chain ends with a takeover of the realm. The issue sums this up briefly as a broker login as any user.

Truly affected, then, are realms with SAML identity brokering through an identity provider where an attacker can get an account. With an open partner or university IdP, that can be any outsider. With a closed corporate IdP, it is every employee, who can then sign in as any other employee, administrators included. Operators of OIDC-only realms, on the other hand, can breathe easy, and according to Red Hat, Red Hat Single Sign-On 7 is not affected either. The fix also changes the IdP endpoint for SAML clients, but neither the advisory nor the analyses describe an impact through that path.

CVEAffectedFixPreconditionCVSS
CVE-2026-117691 (GHSA-c85j-3hq2-q6wj)Keycloak before 26.8.2, Red Hat build of Keycloak before 26.6.9 or 26.4.1826.8.2 (community), Red Hat build of Keycloak 26.6.9 and 26.4.18SAML IdP with signature validation, “Want Assertions Signed” off, own account at the IdP9.1 (critical)
#53354, no CVE, predecessorKeycloak before 26.8.126.8.1as abovenot scored

On versions, the sources look contradictory at first, but that clears up quickly. The GitHub Advisory Database lists neither affected nor patched versions, while the issue carries the labels for 26.8.2, 26.6.9 and 26.4.18. The two lower numbers belong to Red Hat build of Keycloak, because according to endoflife.date the community project only maintains 26.8, and the 26.6 and 26.4 branches ended with 26.6.4 and 26.4.7. Ostorlab lists 26.8.0 to 26.8.1 as well as 26.6 and 26.4 as affected, and older branches with the same check as unpatched.

As of 10 November the flaw is not in the CISA KEV catalog, no attacks in the wild are known, and there is no EPSS score yet. That should not reassure anyone, though, because a public PoC exists. It appeared on 3 November, two days before the fix, still without a CVE number and with a lab environment for Keycloak 26.8.1. Since 26.8.1 already closes the semicolon variant, I attribute it to this flaw, and according to Rescana, CERT Santé also reports an open-source PoC. A second entry that exploit databases list as a PoC is merely a version scanner. For two days, the flaw was thus publicly described without an update being available.

Keycloak 26.8.2 and the backports

The fix arrived on 5 November with Keycloak 26.8.2, the same day as the advisory. It is a pure code change in the server, so you do not have to flip any switch for it to take effect. According to Phase Two, the project no longer maintains the 26.7 branch, so if you run it, you move to 26.8.2. Some confusion came from the release post on keycloak.org, which also names backports in 26.6.9 and 26.4.18. These are version numbers of Red Hat build of Keycloak, and Red Hat shipped both on 7 November: errata RHSA-2026:96039 and RHSA-2026:96040 for 26.6.9 and RHSA-2026:96037 and RHSA-2026:96038 for 26.4.18 explicitly list CVE-2026-117691. There are no community builds with these numbers, which makes Phase Two's contrary statement from release day outdated. For the community image, the only way forward is 26.8.2, while with the Red Hat build you can stay on your branch.

Along with this one fix, though, the update brings a good deal more. 26.8.2 closes 33 security issues, nine of them in the SAML code, and Phase Two counts 14 breaking changes. Three of them hit operators particularly often. The Admin REST API now checks the audience through the new client scope kc-admin-api, which is why service accounts calling the Admin API need this scope explicitly. The SAML broker requires a valid bearer SubjectConfirmation and rejects assertions with ProxyRestriction. And if you let Keycloak link IdP accounts automatically, you now need a verified email address for it. On top of that, processing of SAML 1.1 requests is switched off and SOAP requests are capped in size.

Most of the risk lies in the changes that do not fail loudly. An automation job that maintains clients through the Admin API breaks after the update, and often only whoever waits for its result notices. An identity provider that sends assertions without a matching SubjectConfirmation used to work and is now rejected. Both only show up reliably on staging, with real broker logins through every connected IdP and a run of every job that uses the Admin API. If you run Keycloak through the operator, update the operator and the server image together and then check which image is actually running.

Hardening beyond the patch

Before the individual measures, some orientation helps. In this attack no foreign code runs in Keycloak, and no unusual syscall gives it away. Keycloak accepts a request at a public endpoint and then makes a wrong trust decision. What works, therefore, is whatever ties this decision to a second, independent check, closes the entry channel for manipulated requests, or limits what a forged identity is worth. Anything that hardens the container itself misses the point here.

Requiring signed assertions

With “Want Assertions Signed” you set in the IdP configuration that Keycloak expects a signed assertion. That signature is an XML signature inside the decoded document and is checked against the XML, not against the URL string. A forged, unsigned assertion fails this check, whichever query parameter it came in through. Ostorlab explicitly names the setting as protection, and the flaw requires it to be off.

I consider this the most important hardening, because it turns the principle of the flaw around. One signature with two readings becomes two signatures checked by different parsers. The limit lies with the identity provider, which must actually sign its assertions. Look at its configuration or at a captured response before you flip the switch, because otherwise every login fails. Try the change on staging first, with one real login per IdP.

Closing the Redirect binding at the ingress

Many identity providers deliver their responses via the POST binding. In that case the broker endpoint under /realms/<realm>/broker/<alias>/endpoint needs no GET requests with a SAMLResponse parameter, and you can reject them before they reach Keycloak. Phase Two recommends exactly this for as long as a branch has no fix. In Traefik, you create a dedicated router with higher priority that combines Method, PathRegexp and QueryRegexp and sends such requests to a service that rejects them. According to the documentation, QueryRegexp with the pattern ^.*$ matches any value of a parameter.

For the rule to do what it promises, it has to compare parameter names in decoded form, otherwise it misses exactly the encoded spelling that matters here. Traefik does this. According to its source code, the matcher for Query and QueryRegexp reads parameters via req.URL.Query() and thus through the query parser of the Go standard library, which decodes names. In a short test I ran with Go 1.24, %53AMLResponse and even a fully encoded name ended up under the key SAMLResponse. Because the Traefik documentation is silent on this, send one encoded request against your ingress once the rule is in place.

Two more points belong here. Block only SAMLResponse, because some IdPs send logout requests as SAMLRequest via GET. And check the access log beforehand to see which bindings your IdPs really use. A broader rule that rejects encoded or duplicate SAML parameters mirrors the fix, but according to Ostorlab it can disturb clients with unusual encodings. Red Hat does not list it as a supported workaround either.

A second factor after the broker login

For every identity provider, Keycloak lets you configure a “Post login flow”. The documentation describes it as a flow that triggers additional steps after every login through that IdP, such as a second factor via OTP. This helps against the flaw because the forged response gives the attacker the identity but not the victim's OTP generator. A session is only created once this step has been passed as well.

One detail of the documentation deserves attention. A simple OTP flow asks users without a registered OTP to set it up at the next login, and if the victim has no second factor yet, the attacker simply sets one up in their name. The chapter on authentication flows, however, describes a pattern that prevents exactly this. There, the second factors sit as ALTERNATIVE in a conditional sub-flow with “Condition - user configured”, which only runs when the user already has a second factor. A second sub-flow checks with “Condition - sub-flow executed” whether the first one ran, and otherwise ends the login with “Deny access”. Used as a post login flow, which is my derivation from the two documentation sections, this construction requires the existing second factor and allows no setup in the victim's name.

This comes at a price. Users without a second factor cannot get in through this IdP until they have set one up another way, and anyone who already uses a second factor at the IdP will confirm twice from now on. For administrators and privileged roles I consider that acceptable. Test the flow on staging before rollout, with one account that has a second factor and one that does not.

No admin rights via the SAML broker

How far a taken-over identity reaches is up to you. If an IdP mapper grants administrator or realm-management roles, every flaw in the broker path becomes a route to taking over the realm. I conclude from this that administrative roles should not arise from mappers over IdP attributes, and that the login for administration belongs in a realm without SAML brokering. The effort stays manageable, because at its core it means a few additional accounts that you secure with their own second factor.

Who can obtain signed responses

Without an account at the identity provider, there is no attack. In its mitigation notes, Red Hat therefore advises reviewing and restricting account registration at external identity providers. With an IdP that allows self-registration, this shrinks the circle of possible attackers considerably. It does not close the flaw, however, because employees and partners with an account can still produce the required signed response. Red Hat itself says that these steps only limit the damage.

What does not help here

Many standard hardening measures remain correct but miss this mechanism entirely. seccomp with RuntimeDefault, dropped capabilities, runAsNonRoot, allowPrivilegeEscalation: false and readOnlyRootFilesystem target code that wants to do damage inside the container, and there is no such code here. The same applies to the Pod Security Standard “restricted”, to user namespaces and to a minimal base image. NetworkPolicies and egress restrictions do not apply either, because the attack arrives as a perfectly normal inbound request to an endpoint your users must be able to reach.

The option “Want Assertions Encrypted” is deceptive. It looks like protection, but encryption uses Keycloak's public key, which anyone can read from the metadata. An attacker simply encrypts the forged assertion as well. Encryption protects confidentiality and says nothing about origin. Rotating IdP certificates does not help either, because the attacker needs no key, only a genuine response. And a CVE gate in the pipeline would never have reported the predecessor variant #53354, because it never received a CVE number. If you run Keycloak, read the weaknesses in the release notes as well.

You can find more on ingress rules, tenant separation and update processes in the series TYPO3 on Kubernetes under IT-Grundschutz.

Detection and verification

Whether you are affected shows first in the version, so kc.sh --version or the running image. After that, the configuration decides. Check in every realm whether SAML identity providers are connected and how “Validate Signature” and “Want Assertions Signed” are set there. If signature validation is on and signed assertions are off, the IdP fits the flaw's profile exactly. If signature validation is missing altogether, you have a problem that has nothing to do with this flaw.

Harder is the question of whether someone has already used the flaw. Ostorlab names as indicators GET requests to the broker endpoint in which SAML parameter names are written in encoded form or appear twice. You will only find either if your ingress logs the full query, and you should keep data protection in mind when it does, because a SAMLResponse contains personal data. More telling is the comparison with the identity provider: a broker login in Keycloak for which the IdP knows no login by the same user is suspicious. For this, Keycloak has to store login events, which you enable under “Realm settings” in the “Events” tab with “Save events”. A hit is a reason to look closer, not yet proof, and without query logging a failed attempt may leave no trace at all.

If you suspect an attack, first block the Redirect binding at the ingress or apply the update. Then sign out all sessions of the affected realm, revoke the offline tokens and go through the admin events for changed roles, new clients and new credentials. Events of type “Update Credential” tell you whether someone has set up a second factor in a user's name. Neither the advisory nor the analyses name IoCs, so I derive the search for encoded parameter names from the mechanism.

Recommendation for operators

Act now if Keycloak before 26.8.2 brokers an external SAML identity provider, “Want Assertions Signed” is off there, and people outside your closest team also have accounts at that IdP. Apply the update, or close the Redirect binding at the ingress until then. Switch on signed assertions as soon as you know your IdP delivers them.

Monitoring is enough if your realms use OIDC exclusively, or if your SAML IdPs already send signed assertions that Keycloak also requires. The update still belongs in the next regular maintenance window, if only because of the other 32 fixes in 26.8.2.

Regardless of the individual case, what I say about every flaw applies: patches and updates belong in automation. If Renovate or Dependabot propose new Keycloak images and operator versions as merge requests on their own, and the pipeline tests them against staging with real broker logins, a security release like 26.8.2 is a matter of hours rather than weeks. A cooldown for routine updates protects against compromised releases, and you exempt security updates from it. With a release carrying 14 breaking changes, this process pays off in particular, because the rejected service account at the Admin API then shows up in testing and not in production. If you update by hand instead, you can easily sit on an open hole in your login for weeks during the holiday season and then apply a large bulk update in which nobody can say for sure which change caused which error.

Frequently asked questions about CVE-2026-117691

I only use OIDC. Am I affected?+

No. The flaw sits in the SAML Redirect binding. Affected are realms that broker an external SAML identity provider. The update is still worth it because of the other fixes.

Is 26.6.9 enough instead of 26.8.2?+

Only with Red Hat build of Keycloak. There, 26.6.9 and 26.4.18 contain the fix according to the errata. Community builds with these numbers do not exist, so for the community image 26.8.2 is the only way.

Does encrypting assertions help?+

No. Encryption uses Keycloak's public key, which anyone knows. What helps against the flaw is the signature of the assertion, in other words the option “Want Assertions Signed”.

What should I do if I suspect a hijacked session?+

First close the Redirect binding or apply the update. Then sign out all sessions of the realm, revoke offline tokens and review admin events and newly registered credentials.

Conclusion

CVE-2026-117691 is the second flaw in this code path within a month, and comparing the two fixes is what makes it instructive. The first closed one particular way of splitting the query. The second closes another and, on top of that, requires that the signed value is also the processed one. The lesson reaches well beyond Keycloak and SAML. Wherever a protocol signs a serialized form, processing has to draw on exactly the bytes that were checked, and never on a second reading of the same input. Similar flaws arise wherever two components split the same request differently, as the header bypass in Traefik recently showed as well. It is worth searching your own login paths specifically for places where a trust decision is made in one spot and the data is read in another.

Sources

I review your Keycloak realms for vulnerable SAML integrations, harden broker and ingress and automate the rollout of security updates.

Keycloak audits for identity brokering, configuration of signed assertions and post login flows, ingress rules with Traefik and update automation with Renovate and tested pipelines.

Platform operations instead of consulting on paper: I review, patch and harden your infrastructure continuously.

About the author

Photo of Kai Ole Hartwig.

Kai Ole Hartwig

Freelance DevSecOps consultant · OnlyOle Consulting

Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.