SameSite=Lax — Cookies Missing on Cross-Site POST
Diagnose sessions that work for navigation but vanish on cross-site POSTs and iframes, understand Lax default semantics, and fix flow structure or cookie policy.
Symptoms
- A payment provider POST-back or OIDC callback arrives without the session cookie and logs the user out
- Top-level navigation keeps the session (GET links work) while POST submissions from other sites lose it
- The flow works in older browsers or with SameSite disabled test profiles
Possible causes
- SameSite=Lax (the modern default) sends cookies only on top-level GET navigation, not cross-site POST
- A return-POST handshake (payment callback, legacy SSO) relies on cookie presence that Lax forbids
- An embedded iframe context is neither top-level nor same-site, so Lax withholds the cookie
- A cross-site subresource fetch (XHR to another site) carries no credentials under Lax by design
Diagnosis steps
Compare cookie presence across request types
DevTools → Network → the cross-site POST → Request Headers → Cookie sectionInterpretation
Cookie present on same-site GET, absent on cross-site POST, is the Lax boundary demonstrated in one panel.
Read the cookie attributes
curl -sI https://app.example.com/login | grep -i set-cookieInterpretation
SameSite=Lax or an absent attribute (defaults to Lax in modern browsers) explains POST omission without any bug in the session store.
Classify the flow shape
grep -Rn "redirect\|callback\|returnUrl" src/routes/ | headInterpretation
Whether the third-party returns via POST-back or redirect determines the fix: reshape the flow or explicitly relax the cookie.
Root cause
SameSite=Lax allows cookies only on same-site requests and top-level navigations using safe methods (GET). A cross-site POST — payment callback, form auto-submit from another origin — is neither, so the browser withholds the session cookie and the receiving endpoint treats the user as anonymous.
Fix
Prefer reshaping flows to top-level GET redirects (return a redirect URL instead of a POST-back, or use a one-time token in the URL validated server-side). When POST-back is unavoidable, set SameSite=None; Secure for the specific cookie, accepting the broader exposure deliberately.
Risk note
SameSite=None on the session cookie reopens cross-site request inclusion that Lax closed, raising CSRF exposure. Add token-based CSRF defenses if you relax the attribute.
Verify the fix
The callback flow completes with the session intact; cross-site POSTs carry the cookie only for the deliberately relaxed one, and CSRF protections still block forged requests.
Symptom walkthrough
The split is the tell: session works for every normal navigation, but the moment a third party POSTs back (payment provider, legacy SSO), the request arrives anonymous. The Network panel shows it plainly — same-site GET carries the cookie, cross-site POST carries nothing. And the Set-Cookie line explains why without any server bug:
Set-Cookie: session=abc123; Path=/; HttpOnly; SameSite=Lax
Lax permits top-level GET navigation; it withholds on cross-site POST and in iframes. Browsers defaulting to Lax converted previously-working POST-back flows into silent logouts.
Fix in practice
Reshape the flow when possible — GET redirect with a server-validated one-time token beats POST-back under modern cookie policy:
provider -> redirect GET /payments/callback?token=one-time-id
server: validate token, look up pending operation, apply, redirect onward
When the POST-back is fixed by an external provider, relax the one cookie deliberately and pair it with CSRF defenses:
Set-Cookie: session=abc123; Path=/; HttpOnly;
Secure; SameSite=None
SameSite=None must carry Secure (browsers reject None without it), and every None you set re-admits cross-site sending — so keep the count of relaxed cookies to the minimum the integration truly requires, and verify CSRF tokens still guard state-changing routes.