Cross-Site Request Forgery (CSRF): The Complete Practical Guide
Bypassing SameSite Cookie Restrictions
How to defeat SameSite cookie defenses for CSRF: top-level GET under Lax, the Lax-by-default POST grace window, sibling domains, redirect laundering, and None.
Published
SameSite cookies are the browser-level CSRF defense, and modern browsers default to Lax, which has genuinely reduced classic CSRF. But Lax is not Strict, defaults are not the same as explicit settings, and "same-site" is broader than "same-origin." This page covers what each SameSite value actually blocks and the specific gaps that remain. Practice on a free live target.
What Strict, Lax, and None block
Strict: the cookie is never sent on any cross-site request, including top-level navigations. Strong, but it breaks following inbound links while logged in, so apps often avoid it.Lax: the cookie is sent on top-level navigations that use a safe method (a GET you can see in the address bar), but not on cross-site POSTs or background requests. This is the modern default.None: the cookie is sent on all cross-site requests (and must be markedSecure). This is no CSRF protection at all.
So your first move is to read the session cookie and see which one applies, because each points at a different bypass.
Lax: turn the action into a top-level GET
Lax still allows the cookie on a top-level GET navigation. If a state-changing action is reachable by GET (it should not be, but many are), you simply navigate the victim there, and the cookie rides along under Lax:
<!-- top-level navigation fires on load; Lax sends the cookie -->
<script>location = "https://target.example/account/[email protected]";</script>A background img POST would be blocked under Lax, but a top-level GET navigation is permitted, so a GET-reachable action defeats the Lax default directly. This is why read-only GET is a prevention in its own right.
Lax-by-default: the fresh-cookie POST window
This is the subtle one. A cookie that sets no SameSite attribute is treated as Lax by the browser default, but to avoid breaking login and SSO flows, the browser still sends it on a top-level cross-site POST for a short window after it is issued (around two minutes in current Chrome). A cookie that explicitly sets SameSite=Lax does not get this grace window.
So if the target's session cookie has no explicit SameSite attribute, you can land a cross-site POST by making the victim authenticate (or re-issue the cookie) and then immediately submitting your top-level POST within the window:
<!-- top-level POST within the fresh-cookie window, for a cookie with no explicit SameSite -->
<body onload="document.forms[0].submit()">
<form action="https://target.example/account/email" method="POST">
<input type="hidden" name="email" value="[email protected]">
</form>
</body>Note the precondition precisely: it only works for cookies that never set SameSite, not cookies that chose Lax, and browsers intend to phase the window out, so treat it as opportunistic.
Sibling domains count as same-site
SameSite is scoped to the site (the registrable domain, roughly eTLD+1), not the origin. So a.target.example and b.target.example are the same site, and a request from one to the other is not cross-site at all, so SameSite does not apply. If you control or can inject into any sibling subdomain (a vulnerable subdomain, a user-content subdomain, a subdomain takeover), you can issue requests to the main app as same-site and SameSite blocks nothing.
Client-side open-redirect laundering
If the target has an open redirect that runs client-side, you can launder your request through it so the final request originates from the target's own site. The victim is sent to the target's redirect, which bounces to the state-changing URL, and because that final hop comes from the target itself it counts as same-site. An on-site redirect turns a cross-site request into a same-site one.
None: free CSRF
If the session cookie is set to SameSite=None, the browser sends it on every cross-site request, so SameSite provides no protection whatsoever. At that point CSRF depends entirely on whether a proper token is enforced, and if there is no token, it is plain classic CSRF with a simple auto-submitting form.
Step through it
Read the cookie, then pick the matching bypass: GET-reachable under Lax, the fresh-cookie POST window for a no-SameSite cookie, a sibling domain you control, an on-site open redirect, or None meaning no barrier. Host the payload, have the logged-in victim open it, and confirm the state change fires.
Fixing it
- Set
SameSite=Laxexplicitly (orStrictfor the most sensitive apps) so the cookie never relies on the default and never gets the grace window. - Still require a CSRF token, because SameSite alone leaves the GET, sibling-domain, and redirect gaps.
- Never make state-changing actions reachable by GET.
- Lock down subdomains, since any one of them is same-site to the main app.
Full guidance is in preventing CSRF. Next, see the other header-based defense and how it fails in bypassing Referer defenses, or try a SameSite bypass on a free lab.
← Back to Cross-Site Request Forgery (CSRF): The Complete Practical Guide