Skip to content

fix(sso): stop a SAML LogoutResponse from ending a session it never belonged to - #3324

Merged
marevol merged 1 commit into
masterfrom
fix/saml-unsolicited-logout-response
Aug 20, 2026
Merged

fix(sso): stop a SAML LogoutResponse from ending a session it never belonged to#3324
marevol merged 1 commit into
masterfrom
fix/saml-unsolicited-logout-response

Conversation

@marevol

@marevol marevol commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

What

/sso/logout accepted an unsigned, unsolicited SAML LogoutResponse and ended
whatever session the request carried. This keeps the session for that kind of
message, and reports one bounded line when the message reached a login that was
still live.

Why

/sso/logout is anonymous, and because SAML requires SameSite=none on the
session cookie it is reachable cross-site with the victim's cookie attached.

Auth#processSLO was given null as the request ID, so java-saml skipped the
InResponseTo comparison. With the shipped saml.security.want_messages_signed=false
every remaining check is conditional on an attribute the sender may simply omit:
Issuer, Destination, and the InResponseTo attribute itself. A
LogoutResponse containing nothing but a Success status therefore passed
validation, and processSLO invalidated the session.

The SP has no request ID to give: LogoutAction asks the SSO manager for the
redirect URL and then calls logout(), which invalidates the session that would
have held it. So the binding cannot be restored by storing the ID there.

Measured on a Keycloak-backed setup, against master:

before after
live SAML login, forged LogoutResponse session ended, 0 log lines session kept, 1 WARN
local (non-SAML) login session ended session kept
login still in flight session ended, and the pending AuthnRequest ID with it, so the login could not complete login completes
SP-initiated SLO round trip login ended login ended, no WARN
LogoutRequest naming the session's own user session ended session ended
LogoutRequest naming another user session kept (#3262) session kept

The impact is denial of service rather than escalation: an attacker ends
sessions and, while the request keeps being made, prevents logins from
completing. saml.security.want_messages_signed=true already prevented it,
which is why the start-up banner recommends it — but the shipped default did not.

How

  • A LogoutResponse no longer ends the local session. Nothing is lost by that:
    a legitimate one answers a LogoutRequest this SP sent, and by the time it can
    arrive LogoutAction has already ended the login, so the session it lands on
    is a fresh one. This is the same exposure the NameID comparison closes for a
    LogoutRequest (fix(sso): refuse a SAML LogoutRequest that names another user #3262); a LogoutResponse carries no NameID, and needs none,
    because there is nothing left for it to end.
  • A LogoutResponse that reaches a session which is still logged in is reported.
    That never happens on the path a logout actually takes, so the legitimate round
    trip stays silent. The line is bounded, carries no stack trace and echoes
    nothing the sender supplied — an anonymous endpoint must not let an
    unauthenticated client fill the log.
  • A request carrying both parameters stays on the LogoutRequest branch, decided
    the same way round as Auth#processSLO reads them.

Tests

SamlAuthenticatorTest 75 tests green (3 added), and 235 green across the
related SSO/exception suites.

Verified end to end against a Keycloak IdP: the forged message is refused and
reported, a login in flight completes afterwards, the SP-initiated SLO round trip
still ends the login without a warning, and both LogoutRequest cases behave as
before. Reverting the change turns six of those assertions red, so they are not
vacuous.

…elonged to

/sso/logout is anonymous and, because SAML requires SameSite=none, reachable
cross-site with the victim's session cookie attached. A LogoutResponse arriving
there was passed to Auth#processSLO with no request ID to bind it to, because
the SP has none to give: LogoutAction asks the SSO manager for the redirect URL
and then calls logout(), which invalidates the session that would have held it.
java-saml therefore skips the InResponseTo comparison, and with the shipped
saml.security.want_messages_signed=false every remaining check is conditional
on an attribute the sender may simply omit -- Issuer, Destination, and the
InResponseTo attribute itself.

A LogoutResponse carrying nothing but a Success status was consequently enough
to invalidate whatever session it was pointed at. No signature, no guess at a
NameID, and nothing written to the log. It ended a SAML login, a local one, and
a login still in flight, whose pending AuthnRequest ID went with the session and
left the user unable to log in for as long as the request kept being made.

Nothing is lost by keeping the session: a legitimate LogoutResponse answers a
LogoutRequest this SP sent, and by the time one can arrive the local login has
already been ended, so the session it lands on is a fresh one and invalidating
it ends nothing that was still running. This is the same exposure the NameID
comparison closes for a LogoutRequest, where that comparison costs the sender a
guess; a LogoutResponse carries no NameID, and needs none, because there is
nothing left for it to end.

A LogoutResponse that reaches a session which is still logged in is reported,
since that is a message answering a logout nobody started. The line is bounded,
carries no stack trace and echoes nothing the sender supplied, for the reason
the rest of this endpoint gives: an anonymous endpoint must not let an
unauthenticated client fill the log. A request carrying both parameters stays on
the LogoutRequest branch, which the NameID comparison already guards.
@marevol marevol self-assigned this Aug 20, 2026
@marevol marevol added this to the 15.8.0 milestone Aug 20, 2026
@marevol
marevol merged commit 41f138b into master Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant