From 899de9b58999a3ba721c7d84110c1a917248f56c Mon Sep 17 00:00:00 2001 From: Nick <40026523+Nick-CHI@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:03:41 +0800 Subject: [PATCH] docs(auth-core): state that the fixation revoke skips form_post (#36) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `issue()` and `Login::presented_cookie` implied the presented session is always revoked. It is not: on a form_post callback the SameSite=Lax cookie does not arrive, so presented_cookie is None and the revoke does not fire. That is not exploitable — the cookie is __Host- and HttpOnly, so it cannot be planted cross-site (the attack the revoke defends), and the browser overwrites it anyway; only the old server-side row lingers until expiry. Documented so a reader stops drawing the wrong conclusion, per #36. Firing it on every path (sealing the session id into the flow + a presented_session_id on Login) is the option-1 change left on the issue. Co-Authored-By: Claude Opus 4.8 --- crates/auth-core/src/sessions.rs | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/auth-core/src/sessions.rs b/crates/auth-core/src/sessions.rs index 6a8983c..3950422 100644 --- a/crates/auth-core/src/sessions.rs +++ b/crates/auth-core/src/sessions.rs @@ -174,8 +174,10 @@ pub struct Login<'a> { pub user_id: &'a str, pub ip: Option<&'a str>, pub user_agent: Option<&'a str>, - /// The cookie the login request presented, if any: revoked before - /// the new session exists (fixation). + /// The raw cookie the login request presented, if any: the session it + /// names is revoked before the new one exists (fixation). `None` on a + /// `form_post` callback, where the `SameSite=Lax` cookie does not arrive, + /// so the revoke does not fire there (see [`issue`]; auth #36). pub presented_cookie: Option<&'a str>, /// Authentication-method references (RFC 8176) for this login — /// `passkey` methods say `["user","passkey"]`-style values when the @@ -204,9 +206,21 @@ fn audit(action: &str, user_id: &str) { } /// Issues a session on a successful login: 32 random bytes in the -/// cookie, only the SHA-256 stored, expiry `now + 30 days`. Any -/// session the request presented is revoked first — a new session is -/// always issued at login, so a fixed pre-login value dies here. +/// cookie, only the SHA-256 stored, expiry `now + 30 days`. +/// +/// **Fixation defence, and its one blind spot.** A session whose raw +/// cookie the request presents ([`Login::presented_cookie`]) is revoked +/// first, so a fixed pre-login value dies here. This does not fire on a +/// `form_post` callback (Apple): the session cookie is `SameSite=Lax` and +/// does not arrive on a cross-site POST, so `presented_cookie` is `None` +/// there. That is not a hole — the cookie is `__Host-` and `HttpOnly`, so +/// it cannot be planted cross-site, which is the attack the revoke exists +/// for; the browser has also just overwritten it with the new value. The +/// only residue is the old server-side row living until it expires. Firing +/// the revoke on every path needs the session **id** sealed into the flow +/// at `/start` and a `presented_session_id` on [`Login`] +/// ([auth #36](https://github.com/Factory-Zero/auth/issues/36)); until then, +/// do not read this as "always revokes". /// /// # Errors ///