Bypassing SameSite
Lax top-level GET, Chromium's 120-second Lax+POST window, on-site redirect gadgets against Strict, and schemeful downgrade.
SameSite is a filter, not a wall
SameSite blocks a specific set of request shapes. It does not establish that a request was intended. Everything in this guide follows from finding a shape that is not on the blocked list, or from arranging not to be cross-site at all.
The five approaches, roughly in order of how often they work:
- Use GET.
Laxsends the cookie on a top-level GET navigation, by design. - Be same-site. From a subdomain, nothing is filtered. This also defeats
Strict. - Launder through an on-site gadget. An open redirect on the target's origin makes the final request same-site. Also defeats
Strict. - Hit the Lax+POST window. Chromium only, unset attribute only, 120 seconds.
- Downgrade the scheme. If the cookie is not
Secure, plaintext and HTTPS are cross-site to each other, which cuts both ways.
If none of these apply, the cookie genuinely is not arriving, and you should say so rather than reporting a PoC you have not reproduced.
1. Use GET
The first thing to try, always. Lax permits a top-level navigation with a safe method — so if the endpoint acts on GET, SameSite has not blocked anything.
Ways a POST-looking endpoint turns out to be reachable by GET:
- The route is registered for both. Frameworks make this easy:
app.route(...)with no method list, Rails'match ... via: :all, Django views that do not checkrequest.method. - A method-override parameter is honoured on a GET.
?_method=POSTis checked by some middleware before the method filter runs. See Method and Content-Type Switching. - The framework's safe-method list is wider than you assume. Django and ASP.NET Core exempt
TRACE; Spring exemptsTRACEandOPTIONS. A route reachable by an exempted method has no token check. - A GET-based "convenience" route exists alongside the real one —
/logout,/item/delete?id=,/admin/promote?user=.
When it works, it works in every browser except Strict, with no other conditions. That makes it worth five minutes before anything more elaborate.
2. Be same-site
SameSite compares sites, and a site is the registrable domain. evil.example.com and app.example.com are the same site. Every SameSite value — including Strict — sends the cookie on a request from one to the other.
So any of the following gives you complete SameSite bypass with no technique at all:
- A subdomain takeover — a dangling CNAME to a deprovisioned cloud resource.
- User-controlled content on a subdomain — a docs site, a status page, a legacy app, a customer-branded portal, a
*.example.comwildcard used for tenant subdomains. - XSS anywhere on any subdomain.
- A compromised or forgotten host on the domain.
This also converts the naive double-submit defence into no defence, because a same-site page can write the cookie — see Subdomain Cookie Injection.
When scoping an assessment, enumerate subdomains before concluding that SameSite protects an application. It is a strictly weaker boundary than most teams believe.
3. On-site redirect gadgets
<!-- The only reliable way through SameSite=Strict.
The browser evaluates same-site-ness on the request as it is actually
made. If the FINAL request originates from the target's own origin, it
is same-site, and Strict sends the cookie.
So: navigate the victim to an open redirect ON THE TARGET, and let the
target redirect itself to the state-changing URL.
attacker.example --(cross-site nav)--> app.example/redirect?url=...
app.example --(SAME-SITE nav)--> app.example/transfer?...
The first hop carries no Strict cookie, and does not need to -- the
redirect target is a public endpoint. The second hop is same-site and
carries everything. -->
<script>
location = 'https://app.example/redirect?url=' +
encodeURIComponent('/transfer?to=attacker&amount=5000')
</script>4. The Lax+POST window
Chromium ships a temporary intervention: a cookie with no SameSite attribute at all, set less than 120 seconds ago, is still sent on a cross-site top-level POST.
Be precise about the conditions, because most write-ups get at least one wrong:
- Chromium only. Firefox and Safari have no Lax default, so they send it regardless of age.
- The attribute must be absent. An explicit
SameSite=Laxis never rescued by this. - 120 seconds from when the cookie was set.
- Top-level navigation only, not subresources.
The practical exploit is to make the victim's cookie fresh, then attack immediately:
- Chain a login. Land the victim on a login or SSO flow, then fire the PoC as they return. The cookie is seconds old.
- Force a re-authentication. If the app re-issues the session cookie on any routine action — a step-up prompt, a "session extended" refresh, a password confirmation — that resets the clock.
- Catch a session-refresh pattern. Applications that re-issue the session cookie on every response keep the cookie permanently under two minutes old, which makes this window permanent rather than momentary. That is worth checking for specifically: compare
Set-Cookieacross consecutive responses.
That last case turns a documented-as-temporary intervention into a durable bypass, and it is common in applications that implement sliding session expiry.
5. Schemeful same-site
http://app.example and https://app.example are cross-site to each other under the modern definition. This cuts both ways and is worth understanding in both directions:
As an obstacle. A PoC served over plaintext against an HTTPS target will not reproduce — the request is cross-site and a Secure cookie would not be sent over plaintext anyway. Always match the scheme when building a PoC. Testers lose time to this.
As a bypass. If the session cookie lacks Secure, an attacker with a network position can read it or set it over plaintext on any host in the domain. Cookie-overwrite rules do not consider the Secure flag as part of a cookie's identity, so a non-Secure cookie set over http can shadow a Secure one of the same name. That is a full compromise of naive double-submit and, with the right cookie, of the session itself.
HSTS with includeSubDomains and preloading closes the network-position variant. The __Host- prefix closes the overwrite variant. Use both.
Prevention
- Never change state on GET. This closes the first and most productive bypass outright, and it is a correctness fix as much as a security one.
- Treat subdomains as untrusted unless you control every single one. Use
__Host-prefixed cookies so they cannot be overwritten, and decide deliberately whetherSec-Fetch-Site: same-siteshould be allowed. - Fix open redirects, including client-side ones. Allowlist redirect targets; do not merely check that the value starts with
/, since//evil.exampleand/\evil.exampleboth escape that check. - Set
SameSiteexplicitly. An absent attribute is what makes the Lax+POST window applicable at all, and it makes your behaviour Chromium-specific. - Set
Secureand enable HSTS withincludeSubDomains. - Keep a token underneath all of it. Every bypass on this page is defeated by a synchronizer token, because none of them gives the attacker the ability to read the target's HTML. That is the argument for Defense in Depth.
Related
Exactly when Lax, Strict, None, and an absent attribute attach a cookie to a cross-site request — and why the three engines disagree.
Overwriting the CSRF cookie from a sibling subdomain, why SameSite offers zero protection here, and why __Host- is the fix.
img, script, link, scripted link clicks, and top-level navigation — and why only the last one still carries a cookie.
What still works and what died with Lax-by-default — the honest, browser-by-browser exploitability verdict, plus the techniques that are now purely historical.
GET and POST routing, _method and X-HTTP-Method-Override, and body parsers that ignore the declared content type.
A server-side token bound to the session, plus the lifecycle questions: per-request versus per-session, rotation, BREACH masking, and back-button breakage.
The layering order — SameSite=Lax, __Host- prefix, Fetch Metadata rejection, then a token — plus re-authentication for high-value operations.