mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 14:21:37 +00:00
harden(auth/sessions): CSRF rotation on logout closes HIGH-2 fourth call site
Audit 2026-05-11 Fix 13 closure. The HIGH-2 closure on dev/auth-bundle-2 documented four RotateCSRFTokenForActor call sites — login completion (fresh by construction), Assign/Revoke RoleToKey (wired at internal/api/handler/auth.go:498 + 546), Logout, and an explicit operator endpoint. The 2026-05-11 adversarial review observed only 3 of the 4: Logout did NOT rotate the actor's sibling sessions post-revoke. Threat closed: a token captured pre-logout (browser DevTools, malicious extension, session-storage leak) could be replayed against the user's other-device/other-browser sessions until those sessions hit their own idle/absolute expiry. Rotation on logout defeats this — the captured token is dead the moment the user clicks 'Sign out' anywhere. What this changes: * internal/api/handler/auth_session_oidc.go::SessionMinter interface gains RotateCSRFTokenForActor(ctx, actorID, actorType string) int. Nil-safe semantics by convention — the production wiring is *session.Service which already implements the method; rotation NEVER errors (returns int count, swallows per-row failures via the underlying Service.RotateCSRFToken) so it can't block the surrounding Revoke that triggered it. * internal/api/handler/auth_session_oidc.go::Logout calls RotateCSRFTokenForActor after Revoke(sess.ID) succeeds. The auth.session_revoked audit row gains a csrf_rotated detail key carrying the count so SOC/SIEM can correlate logout events with CSRF churn on sibling sessions. * The no-cookie + invalid-cookie 204 short-circuit paths skip rotation. No session row exists to rotate against; the caller is already unauthenticated. Rotation on those paths would do nothing useful and pollute the audit log. Test coverage in internal/api/handler/auth_session_oidc_test.go: * TestLogout_RotatesCSRFForActor — happy path. Mocks rotateCSRFReturnCount=2; asserts Revoke fires before rotation, rotation fires exactly once with caller's (actor_id, actor_type), audit details carry csrf_rotated=2. * TestLogout_NoCookie_SkipsCSRFRotation — pins the 204 short-circuit branch when there's no cookie. Rotation count stays at 0. * TestLogout_InvalidCookie_SkipsCSRFRotation — pins the 204 short-circuit branch when Validate rejects the cookie. Same rationale: no session row, no rotation. The stubSession test fake gains RotateCSRFTokenForActor with call-recording fields; the phase5StubAudit gains a details slice append-aligned 1:1 with events so the happy-path test can index into the latest entry and assert the count. Spec Phase 3 (explicit operator endpoint) — intentionally NOT shipped. The three automatic triggers (login + role- mutation + logout) cover the HIGH-2 threat model; operators who want a nuclear option can use the existing RevokeAllForActor flow which forces re-login → fresh session → fresh CSRF. Adding a dedicated POST /api/v1/auth/sessions/ rotate-csrf admin endpoint would be defense-in-depth without new attack-surface coverage. Documented in the audit-doc annotation. Verify gate: * gofmt -l — clean * go vet ./internal/api/handler/... — clean * go build ./cmd/server/... ./internal/... — clean (production *session.Service satisfies the extended interface out of the box) * go test -short -count=1 ./internal/api/handler/... ./internal/auth/session/... — all green; 3 new Logout cases + the 2 pre-existing Logout cases all pass. Audit doc annotation at cowork/auth-bundles-audit-2026-05-10.md flips the HIGH-2 row from 'CLOSED 2026-05-10 (3/4 call sites wired)' to 'A-B-3 verified 2026-05-11: HIGH-2 fully closed across all four documented call sites.' Refs cowork/auth-bundles-fixes-2026-05-11/13-verify-logout-csrf-rotation.md.
This commit is contained in:
@@ -4,6 +4,31 @@
|
||||
|
||||
### Security
|
||||
|
||||
- **CSRF rotation on logout closes HIGH-2 fourth call site (Audit 2026-05-11 Fix 13).**
|
||||
The HIGH-2 closure (`dev/auth-bundle-2`) documented four
|
||||
`RotateCSRFTokenForActor` call sites: login completion (fresh by
|
||||
construction), Assign/RevokeRole on role-mutation (wired), Logout, and
|
||||
an explicit operator endpoint. The 2026-05-11 review verified only 3
|
||||
of the 4 — Logout did NOT rotate the actor's sibling sessions
|
||||
post-revoke, leaving a window where a token captured pre-logout
|
||||
(browser DevTools, malicious extension, session-storage leak) could
|
||||
be replayed against the user's other-device/other-browser sessions
|
||||
until those sessions hit their own idle/absolute expiry.
|
||||
`SessionMinter` interface extended with `RotateCSRFTokenForActor`;
|
||||
`Logout` invokes it after `Revoke(sess.ID)` succeeds. The
|
||||
`auth.session_revoked` audit row gains a `csrf_rotated` detail key
|
||||
carrying the rotated count so SOC / SIEM can correlate logout events
|
||||
with CSRF churn. The no-cookie + invalid-cookie 204 short-circuit
|
||||
paths skip rotation (no session row to rotate against). 3 regression
|
||||
tests in `internal/api/handler/auth_session_oidc_test.go` pin the
|
||||
happy path + the two short-circuit branches. The explicit operator
|
||||
endpoint (4) remains intentionally unbuilt — the three automatic
|
||||
triggers (login + role-mutation + logout) cover the threat model;
|
||||
operators who want a nuclear option can use the existing
|
||||
`RevokeAllForActor` flow which forces re-login → fresh session →
|
||||
fresh CSRF. **HIGH-2 fully closed across all four documented call
|
||||
sites.**
|
||||
|
||||
- **Scope-aware actor-role revoke (Audit 2026-05-11 A-4).**
|
||||
HIGH-10 made it possible to grant the same role to the same actor at
|
||||
multiple scopes (e.g. `r-operator` on `profile=p-acme` AND `profile=p-globex`)
|
||||
|
||||
Reference in New Issue
Block a user