fix(auth): login hardening cluster (pre-login op_session revoke, multi-keyed rate limit, 2FA cookie hygiene) #289

Merged
YousifShkara merged 1 commit from fix/BUNYIP-255-login-hardening into main 2026-06-30 06:42:35 +02:00
Owner

BUNYIP-255: three login-surface hardening items from the 2026-06-30 audit.

Pre-login op_session invalidation. establish_op_session previously minted a new sid via create_op_session without dropping any pre-existing bunyip_op_session cookie the browser was already carrying. A sibling-subdomain XSS that planted a sid before login (or a stale cookie from an unrelated browser session) would keep that sid live alongside the new one, classic session-fixation surface. Add a new OidcProvider::revoke_op_session_by_sid (runtime query so no .sqlx/ cache regen) and call it from establish_op_session at the top of every login path; best-effort revoke so a no-op or DB hiccup never blocks the login.

Multi-keyed login rate limit. POST /v1/auth/login previously rate-limited on per-email only, so an attacker spraying one password across many victim emails dodged the cap entirely (each victim had a fresh per-email budget). Add per-IP and per-IP-per-email windows alongside the existing per-email one, all using the existing RateLimitConfig::LOGIN knob (5 / 60 s). The aggregate guessing budget from any one IP is now bounded independently of which email it targets; the per-email cap stays in place as the per-account fallback.

bunyip_2fa cookie via helpers. The challenge cookie was set + cleared at four sites via raw format! strings (bunyip_2fa=...; Path=/; HttpOnly; SameSite=Lax) that omitted Secure (HTTPS-only on production) and Max-Age (the cookie outlived the challenge token's JWT exp), and the clears omitted matching attributes so browsers might not recognize them as deletes. Add bunyip_2fa_cookie_set / bunyip_2fa_cookie_clear helpers + a Config::use_secure_cookies() predicate (api_public_origin.starts_with("https://")). All four sites route through the helpers so Secure, HttpOnly, SameSite=Lax, Max-Age=600 (the JWT exp window) land consistently and the clears actually clear.

#BUNYIP-255

BUNYIP-255: three login-surface hardening items from the 2026-06-30 audit. Pre-login op_session invalidation. `establish_op_session` previously minted a new sid via `create_op_session` without dropping any pre-existing `bunyip_op_session` cookie the browser was already carrying. A sibling-subdomain XSS that planted a sid before login (or a stale cookie from an unrelated browser session) would keep that sid live alongside the new one, classic session-fixation surface. Add a new `OidcProvider::revoke_op_session_by_sid` (runtime query so no `.sqlx/` cache regen) and call it from `establish_op_session` at the top of every login path; best-effort revoke so a no-op or DB hiccup never blocks the login. Multi-keyed login rate limit. `POST /v1/auth/login` previously rate-limited on per-email only, so an attacker spraying one password across many victim emails dodged the cap entirely (each victim had a fresh per-email budget). Add per-IP and per-IP-per-email windows alongside the existing per-email one, all using the existing `RateLimitConfig::LOGIN` knob (5 / 60 s). The aggregate guessing budget from any one IP is now bounded independently of which email it targets; the per-email cap stays in place as the per-account fallback. bunyip_2fa cookie via helpers. The challenge cookie was set + cleared at four sites via raw `format!` strings (`bunyip_2fa=...; Path=/; HttpOnly; SameSite=Lax`) that omitted `Secure` (HTTPS-only on production) and `Max-Age` (the cookie outlived the challenge token's JWT exp), and the clears omitted matching attributes so browsers might not recognize them as deletes. Add `bunyip_2fa_cookie_set` / `bunyip_2fa_cookie_clear` helpers + a `Config::use_secure_cookies()` predicate (`api_public_origin.starts_with("https://")`). All four sites route through the helpers so `Secure`, `HttpOnly`, `SameSite=Lax`, `Max-Age=600` (the JWT exp window) land consistently and the clears actually clear. #BUNYIP-255
fix(auth): login hardening cluster (pre-login op_session revoke, multi-keyed rate limit, 2FA cookie hygiene)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 55s
Check / fmt + clippy + build + tests (pull_request) Successful in 10m57s
Create release / Create release from merged PR (pull_request) Has been skipped
b098dc7d1a
BUNYIP-255: three login-surface hardening items from the 2026-06-30 audit.

Pre-login op_session invalidation. `establish_op_session` previously minted a new sid via `create_op_session` without dropping any pre-existing `bunyip_op_session` cookie the browser was already carrying. A sibling-subdomain XSS that planted a sid before login (or a stale cookie from an unrelated browser session) would keep that sid live alongside the new one, classic session-fixation surface. Add a new `OidcProvider::revoke_op_session_by_sid` (runtime query so no `.sqlx/` cache regen) and call it from `establish_op_session` at the top of every login path; best-effort revoke so a no-op or DB hiccup never blocks the login.

Multi-keyed login rate limit. `POST /v1/auth/login` previously rate-limited on per-email only, so an attacker spraying one password across many victim emails dodged the cap entirely (each victim had a fresh per-email budget). Add per-IP and per-IP-per-email windows alongside the existing per-email one, all using the existing `RateLimitConfig::LOGIN` knob (5 / 60 s). The aggregate guessing budget from any one IP is now bounded independently of which email it targets; the per-email cap stays in place as the per-account fallback.

bunyip_2fa cookie via helpers. The challenge cookie was set + cleared at four sites via raw `format!` strings (`bunyip_2fa=...; Path=/; HttpOnly; SameSite=Lax`) that omitted `Secure` (HTTPS-only on production) and `Max-Age` (the cookie outlived the challenge token's JWT exp), and the clears omitted matching attributes so browsers might not recognize them as deletes. Add `bunyip_2fa_cookie_set` / `bunyip_2fa_cookie_clear` helpers + a `Config::use_secure_cookies()` predicate (`api_public_origin.starts_with("https://")`). All four sites route through the helpers so `Secure`, `HttpOnly`, `SameSite=Lax`, `Max-Age=600` (the JWT exp window) land consistently and the clears actually clear.

#BUNYIP-255
YousifShkara deleted branch fix/BUNYIP-255-login-hardening 2026-06-30 06:42:35 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!289
No description provided.