active? was only checked at the password step of sign-in. A user disabled
afterwards could (a) still complete the 2FA step and mint a valid session, and
(b) keep using any existing session until natural expiry, because per-request
auth only checked session expiry, not user status.
Three enforcement points:
- Mid-flow guard: verify_totp and webauthn_verify re-check active? before
start_new_session_for, clearing the pending session and rejecting if disabled.
- Request-time guard: find_session_by_cookie now uses Session.for_active_user,
so a session whose user is disabled no longer authenticates (authoritative,
catches any disable path including direct DB changes).
- Immediate cleanup: User#revoke_sessions_when_deactivated destroys a user's
sessions when status changes away from active, so access is revoked everywhere
at once rather than on the next request.
Tests cover the mid-flow TOTP rejection, request-time rejection of an existing
session after disable, session destruction on disable, and that unrelated
updates leave sessions intact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
backchannel_logout_uri was validated only for scheme/HTTPS, so an admin (or a
compromised admin account) could point it at internal infrastructure — cloud
metadata (169.254.169.254), loopback, or RFC1918 hosts — and every user logout
would fire a server-side POST there.
Add PrivateAddressCheck (app/lib) and apply it as defense-in-depth:
- Application validation rejects URIs whose host is, or is a literal, internal
address (loopback / private / link-local / 0.0.0.0 / localhost / metadata
hostnames). Fast, DNS-free, immediate admin feedback.
- BackchannelLogoutJob re-checks at request time WITH DNS resolution and aborts
(no retry) if the host resolves to a non-public address — covering URIs that
predate the validation and public hostnames pointed at internal IPs.
Tests cover the address classification, the model validation, and updates an
existing test that used a localhost logout URI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>