Back to the journal

The Timestamp Keycloak Forgot to Check: CVE-2026-1190

Keycloak · SAML brokering · CVE-2026-1190 · CVSS 3.1 Low

SAML has two places that tell you when an assertion stops being valid. Keycloak, acting as a SAML broker, checked one of them and quietly ignored the other. That is the whole bug. It is a low severity finding, 3.1 on the CVSS scale, and i want to be honest about that up front rather than dress it up. But it is a clean example of how a spec that says “validate this” turns into a real gap the moment one check goes missing.

Red Hat assigned it CVE-2026-1190 and credits the report to me and Bettag Systems. Here is what was actually wrong.

The setup

When Keycloak is configured as an identity broker, it is a SAML client. Your users log in at some upstream IdP, that IdP sends a signed SAML Response back to Keycloak, and Keycloak decides whether to trust it and create a session.

A SAML assertion carries time bounds in two different elements, and they are not the same thing:

  • The Conditions element has a NotBefore / NotOnOrAfter pair. This is the validity window of the assertion as a statement.
  • Inside Subject > SubjectConfirmation > SubjectConfirmationData, a bearer confirmation carries its own NotOnOrAfter, plus Recipient and InResponseTo. This is the validity window of using this assertion to confirm this subject, at this endpoint, in response to this request.

The Web Browser SSO profile is explicit about the second one. For the bearer subject confirmation method, the relying party must verify that the SubjectConfirmationData NotOnOrAfter has not passed, that the Recipient matches an endpoint it owns, and that InResponseTo matches the request it sent. These are not optional niceties, they are the conditions under which a bearer assertion is allowed to confirm a subject at all.

What Keycloak actually validated

Keycloak validated the Conditions window correctly. It ran the SubjectConfirmationData through a check too, so at first glance the confirmation data looked handled. But look at what that check actually covered in the brokering path (services/.../broker/saml/SAMLEndpoint.java, before the fix):

1
2
3
4
5
6
7
8
9
10
11
12
13
SubjectConfirmationType subjectConfirmationElement = subjectElement.getConfirmation().get(0);

if (subjectConfirmationElement != null) {
SubjectConfirmationDataType subjectConfirmationDataElement =
subjectConfirmationElement.getSubjectConfirmationData();

if (subjectConfirmationDataElement != null) {
if (subjectConfirmationDataElement.getInResponseTo() != null) {
// InResponseTo present but empty -> reject
// InResponseTo does not match expected request ID -> reject
}
}
}

InResponseTo was checked. NotOnOrAfter was not. Neither was NotBefore, neither was Recipient. The one timestamp that bounds how long a bearer confirmation stays usable was read straight past.

There is a second, quieter problem in the same snippet. It takes getConfirmation().get(0), the first confirmation element, regardless of its method. SAML allows multiple SubjectConfirmation elements with different methods. Grabbing index zero and hoping it is the bearer one is not the same as finding the bearer confirmation and validating it as a bearer confirmation.

Why the missing check matters, and why it is still Low

The obvious question is why this is not catastrophic. The answer is signatures. A SAML Response from a trusted IdP is signed, and Keycloak verifies that signature. You cannot sit in the middle, bump the SubjectConfirmationData NotOnOrAfter out by a year, and have it accepted, because editing the assertion breaks the signature.

So the realistic impact is narrow: the SubjectConfirmationData expiry is an independent, shorter-lived bound that the SAML profile expects the relying party to enforce on top of Conditions. When Keycloak skips it, an assertion stays usable for the full Conditions window even in cases where the confirmation data was meant to close that window earlier. That is a replay/validity-window issue, not an authentication bypass, which is exactly why it lands at CVSS 3.1 with AC:H and UI:R. Red Hat filed it under CWE-112, missing XML validation, and the GitHub advisory adds CWE-613, insufficient session expiration. Both are fair.

i am not going to pretend this is a domain-takeover. It is a standards-conformance gap in security-critical code, found by reading what the profile requires and checking whether the implementation actually does it. Sometimes that is the whole job.

The fix

The upstream fix is commit f0381f8, “Check SubjectConfirmationData element for bearer type”, merged for the 26.5.3 line and backported (Red Hat build of Keycloak 26.4.10). It does two things.

First, it stops trusting index zero and actually selects the bearer confirmation:

1
2
3
SubjectConfirmationType subjectConfirmationElement = subjectElement.getConfirmation().stream()
.filter(c -> JBossSAMLURIConstants.SUBJECT_CONFIRMATION_BEARER.get().equals(c.getMethod()))
.findFirst().orElse(null);

Second, it routes the confirmation data through a new SubjectConfirmationDataValidator that checks the things the old code never did:

1
2
3
4
5
6
7
8
9
10
SubjectConfirmationDataValidator.Builder scdvb =
new SubjectConfirmationDataValidator.Builder(assertion.getID(), subjectConfirmationDataElement, destinationValidator)
.inResponseTo(expectedRequestId)
.clockSkewInMillis(1000 * config.getAllowedClockSkew());
if (responseType.getDestination() != null) {
scdvb.allowedRecipient(responseType.getDestination());
}
if (!scdvb.build().isValid()) {
return false;
}

Inside that validator, the expiry check finally happens, reusing the same expiration logic as Conditions and honoring the configured clock skew:

1
2
3
4
5
6
7
8
9
if (!ConditionsValidator.validateExpiration(assertionId,
subjectConfirmationData.getNotBefore(),
subjectConfirmationData.getNotOnOrAfter(),
now, clockSkewInMillis)) {
return false;
}
if (!validateRecipient()) {
return false;
}

So the fix is not just “add one timestamp comparison.” It brings the brokering path in line with what the bearer profile always required: pick the bearer confirmation, validate its NotBefore/NotOnOrAfter, validate its Recipient, validate its InResponseTo. The old code did a quarter of that and looked complete.

The takeaway for anyone doing SSO

If you run Keycloak as a SAML broker, this is fixed from 26.5.3 onward and in Red Hat build 26.4.10, so update and move on. The more useful lesson is architectural: whenever a protocol gives you two overlapping validity windows, assume they exist for different reasons and that skipping one is a real weakness, not a redundant check you optimized away. SAML Conditions and SubjectConfirmationData bound different things. An implementation that only enforces the first is not a stricter version of the spec, it is a looser one.

The commit is f0381f8 and the issue is keycloak#45646 if you want to read the validator in full. It is short, and the test file next to it is a decent reference for what a correct bearer check looks like.