fix(express)!: clear cookies with the set-path attributes and drop GET state changes#103
Merged
Merged
Conversation
…T state changes clearSessionCookie and clearAllCookies emitted a clearing header with no secure or sameSite while setSessionCookie sets both, defaulting to Secure; SameSite=None. Express's clearCookie only forces expires, so the clearing header was rejected by browsers in a cross-site response, which is the deployment the defaults target. Since requireAuth verifies the cookie locally and never consults the auth API, a 204 logout revoked the session upstream while the signed cookie stayed valid for adopter routes until its own TTL expired. Both clear helpers now take the CookieSigner options so clearing mirrors setting. BREAKING CHANGE: GET /logout and GET /magic-link are removed. Both were state-changing simple requests, so with SameSite=None and no CSRF or Origin check an img tag on any page could revoke every session or flood a mailbox with magic links. GET /logout was already deprecated in favor of DELETE /logout/all. GET /magic-link had no non-GET sibling, so it becomes POST /magic-link rather than being dropped. The existing cookie test only inspected the set path, which is why the clearing defect shipped. The clear path is now asserted directly. Closes #96 Closes #97
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #96
Closes #97
#96: logout did not clear cookies in cross-site deployments (release blocker)
clearSessionCookie/clearAllCookiescalledres.clearCookie(name, { domain, path: "/" })with nosecure/sameSite, whilesetSessionCookiein the same file sets both and defaults toSecure; SameSite=None. Express'sclearCookieonly forcesexpires, so the clearing header was rejected by browsers in a cross-site response context, which is exactly the deployment the defaults target.Because
requireAuthverifies the cookie locally and never consults the auth API, a 204 logout revoked the session upstream while the signed cookie survived in the browser and stayed fully valid for adopter routes until its own TTL expired.Both helpers now take
CookieSignerOptionsand pass{ secure, sameSite, domain, path: "/" }, so clearing mirrors setting. All three call sites are updated:handlers/logout.ts,handlers/me.ts, and the clear-cookies branch inmiddleware/ensureCookies.ts.#97: CSRF via GET /logout and GET /magic-link
Both were state-changing simple requests. With
SameSite=Noneand no CSRF or Origin check, an<img src>on any page could revoke all of a victim's sessions or flood their mailbox with magic links.GET /logoutis removed.DELETE /logoutandDELETE /logout/allalready cover it and it was documented as deprecated. DELETE forces a preflight, so this vector is fully closed.GET /magic-linkbecomesPOST /magic-link. The issue states it already has a POST sibling; it does not.GET /magic-linkwas the only route that requests a magic link, and the handler takes no body or query (identity comes from the pre-auth cookie). Removing it outright would have deleted magic-link login, so it is converted to POST instead, which the issue lists as the alternative fix.Note on residual risk: POST removes the zero-interaction
<img src>vector, but a scripted cross-site form POST withContent-Type: text/plainis still a simple request and can reach the route. Fully closing magic-link CSRF needs the Origin check below.Follow-up (deliberately not in this PR)
An
Origin/Sec-Fetch-Siteallowlist check in the router whensameSite === "none". That covers the remaining POST/DELETE surface (MFA disable, credential deletion), which widens for adopters runningcors({ origin: true, credentials: true }), and closes the residual magic-link vector noted above.Tests
The reason #96 shipped is that
cookieSecurity.test.jsfiltered toset-cookieheaders on the SET path only and never asserted anything about clearing.Added to
cookieSecurity.test.js: the clear path emitsSecureandSameSite=Nonewith an epochExpires, trackscookieSecure: falsedown toSameSite=Lax, and honors an explicitcookieSameSiteoverride.Verified these are not vacuous. Reverting only the
cookie.tschange makes the three new tests fail while all five pre-existing set-path tests still pass, which reproduces the original coverage gap.Added to
logoutRoutes.test.js:GET /logoutandGET /magic-linkno longer route, andPOST /magic-linkstill proxies upstream. TheGET /magic-linkcase sends a valid pre-auth cookie on purpose, since without it the cookie middleware answers 400 before routing and the test would pass whether or not the route existed.Updated
messagingDelivery.test.jsto POST. The upstream fetch to the auth API remains GET; only the adapter's inbound verb changed.Verification
Breaking change
Changeset is
minor, matching the 0.x breaking channel used by #89, #92, and #93. Adopters callingGET /auth/logoutmove toDELETE /auth/logout/all; adopters callingGET /auth/magic-linkmove toPOST /auth/magic-link. README updated. This also affects the client SDKs, which need the same verb changes.