Skip to content
highCVSS 8.1CWE-1275A01:2021 – Broken Access Control

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:

  1. Use GET. Lax sends the cookie on a top-level GET navigation, by design.
  2. Be same-site. From a subdomain, nothing is filtered. This also defeats Strict.
  3. Launder through an on-site gadget. An open redirect on the target's origin makes the final request same-site. Also defeats Strict.
  4. Hit the Lax+POST window. Chromium only, unset attribute only, 120 seconds.
  5. 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 check request.method.
  • A method-override parameter is honoured on a GET. ?_method=POST is 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 exempts TRACE and OPTIONS. 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.com wildcard 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

HTMLVulnerable
<!-- 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=Lax is 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-Cookie across 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 whether Sec-Fetch-Site: same-site should be allowed.
  • Fix open redirects, including client-side ones. Allowlist redirect targets; do not merely check that the value starts with /, since //evil.example and /\evil.example both escape that check.
  • Set SameSite explicitly. An absent attribute is what makes the Lax+POST window applicable at all, and it makes your behaviour Chromium-specific.
  • Set Secure and enable HSTS with includeSubDomains.
  • 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.