Lax by default since 2020·a 120-second grace period nobody advertised·SameSite=None is useless without Secure·two name prefixes, free hardening

What Chrome 80 changed, and why your SSO broke

Before February 2020, a cookie without a SameSite attribute went everywhere. Every iframe, every tracking pixel, every cross-site form POST carried it. Chrome 80 hit stable on 4 February 2020 and flipped the default: a cookie with no SameSite attribute is now enforced as if it were Lax. The rollout got paused in April 2020 because nobody wanted to break payment flows and government portals in the middle of a pandemic, then restarted in July 2020 and finished that summer.

One detail that matters later: the cookie is not rewritten to SameSite=Lax. It gets Lax-by-default enforcement, which the specification (the long-running rfc6265bis draft) treats as a separate state from an explicit Lax attribute. Writing nothing and writing SameSite=Lax are not identical, and the difference is where a whole class of SSO bugs lives.

The other thing worth internalising early: SameSite operates on sites, not origins. A site is the registrable domain, eTLD+1 from the Public Suffix List. So app.example.com and staging.example.com are the same site and swap cookies happily, while alice.github.io and bob.github.io are different sites because github.io is on the Public Suffix List. Ports are irrelevant. The scheme is not: since Chrome 89, http and https count as different sites, a change shipped under the name schemeful same-site.

Strict, Lax and None, precisely

ContextStrictLaxNone
Same-site requestsentsentsent
Link click from another sitenot sentsentsent
Cross-site POST formnot sentnot sentsent
iframe, image, fetch, XHRnot sentnot sentsent

Strict has a user-visible cost that surprises people: a user who clicks a link to your app from Slack, an email or Google search lands on the logged-out version of the page, then reloads and is suddenly logged in. Nothing is broken, the cookie simply was not sent on that first navigation. The usual fix is two cookies. A Lax cookie identifies the user well enough to render a personalised page, and a second Strict cookie is required by anything that writes.

The other two attributes are simpler and get confused with SameSite constantly. HttpOnly means document.cookie cannot read or write it, which is the single most effective mitigation against a stolen session via XSS. Secure means the cookie is only ever sent over HTTPS. Neither has anything to do with cross-site behaviour, and neither encrypts anything: the value is plain text on the wire under TLS and plain text in the browser’s cookie store, which is worth remembering if you were planning to base64 the session payload and call it protected (base64 is not encryption, ever).

The 2-minute exception nobody tells you about

Here is the one that eats afternoons. Chromium ships a compatibility intervention called Lax-allowing-unsafe, or Lax+POST internally: a cookie that is at most 120 seconds old is still sent on a top-level cross-site POST, despite Lax-by-default. It exists because SAML and OpenID Connect flows POST the response back to your site from the identity provider, and without the exception every enterprise SSO login on the web would have died on 4 February 2020.

Two conditions that people miss:

  • It only applies to cookies with no explicit SameSite attribute. Set SameSite=Lax yourself and you opt out of the grace period. Writing the attribute makes your cookie stricter than writing nothing, which is the opposite of what most developers assume.
  • Top-level POST only. A cross-site POST inside an iframe does not qualify, no matter how fresh the cookie is.

Chromium has described the window as temporary and slated for removal since 2019. It is still shipping in 2026. Firefox tracked the same 120-second tolerance in Bugzilla bug 1608384. Assume it will disappear, and never let a flow depend on it. It also produces the most annoying bug signature there is, working perfectly when you test the login immediately and failing when a tester leaves the tab open for five minutes. If you want to see the real behaviour, Chrome can disable the intervention with the SameSiteDefaultChecksMethodRigorously feature flag.

SameSite=None without Secure is thrown away

If you need a cookie in a third-party context, SameSite=None is the only option, and it requires Secure. Not "should have". Browsers reject the entire cookie when the pair is incomplete, so the failure mode is no cookie at all rather than a wrongly scoped one. That is deliberate, and honestly the right call, but it means a missing Secure shows up as a login that silently never sticks. Chrome carves out an exception for http://localhost so local development still works.

The uglier problem is clients that predate the value. iOS 12, macOS 10.14 and a handful of Java and .NET cookie parsers treat an unrecognised SameSite value as Strict, so sending None to them is worse than sending nothing. Apple confirmed the CFNetwork bug in its developer forums and did not backport the fix. The standard workaround, still in use in payment and SSO stacks, is to set the cookie twice: once modern with SameSite=None; Secure, and once under a legacy name with no SameSite attribute at all, then read whichever arrives.

Safari plays by its own rules

Getting your SameSite attributes right does not buy you a working third-party cookie in Safari. Safari 13.1, released in March 2020, blocks all third-party cookies by default through Intelligent Tracking Prevention, regardless of what you set. If your architecture needs a cookie in an iframe on someone else’s domain, Safari users need the Storage Access API and an explicit user gesture, not a better cookie attribute.

Two ITP limits that catch first-party code too:

  • Cookies written by JavaScript expire after 7 days. ITP caps anything set through document.cookie at a seven-day lifetime. Cookies sent by your own server in a Set-Cookie response header keep the expiry you gave them. So a "remember me" flag written in JS quietly dies after a week, only in Safari, and only for users who did not come back in time.
  • Script-writable storage gets deleted after 7 days without interaction. That includes LocalStorage, IndexedDB, service worker registrations and their caches. WebKit later extended the seven-day cap to cookies set via CNAME-cloaked responses, which killed the most popular workaround.

This is the single strongest argument for keeping the session in an HttpOnly cookie set by the server instead of a token in LocalStorage, and it is a point we make at more length in where to store a JWT.

CHIPS: cookies that work in an embed without tracking

CHIPS (Cookies Having Independent Partitioned State) adds one attribute, Partitioned, and one idea: the cookie gets a separate jar per top-level site. Your support widget embedded on shop-a.com and on shop-b.com sets the same cookie name and gets two completely separate values, with no way to correlate the two visits. Chrome shipped it in 2023.

Rules of use: a partitioned cookie must also be Secure and SameSite=None, and the spec recommends the __Host- prefix so it is locked to one host with Path=/. If you build an embeddable widget, a checkout iframe or a CMS preview frame, this is the mechanism to design around. It also survived the Privacy Sandbox cull, which makes it the safest bet on the list.

The third-party cookie phaseout that never happened

Worth having the timeline straight, because half the advice online is written against a deadline that no longer exists. Google announced the phaseout in January 2020 targeting 2022, moved it to 2023, then to 2024, then in July 2024 said it would not deprecate third-party cookies at all. April 2025 removed the last hedge: no standalone user prompt either, just the existing controls in Chrome settings. In October 2025 Google retired the Privacy Sandbox advertising APIs themselves, including Topics, Protected Audience and Attribution Reporting, with the deprecation starting in Chrome 144 in January 2026 and completing with Chrome 150 in July 2026. CHIPS and the related storage partitioning work stayed.

What that changes for you: nothing, mostly. Safari has blocked third-party cookies by default since 2020 and Firefox partitions them through Total Cookie Protection, so a flow that needs one is already broken for a large chunk of real traffic. Chrome keeping them is a reprieve for ad tech, not a design pattern.

__Host- and __Secure-: free hardening almost nobody uses

Cookie name prefixes are the best effort-to-payoff ratio in this whole article. The browser enforces rules based on the name, and refuses to store the cookie if they are not met.

PrefixBrowser enforcesBuys you
__Secure-Secure attribute, set over HTTPSA plain-HTTP page cannot overwrite it
__Host-Secure, Path=/, no Domain attribute, set over HTTPSLocked to the exact host, no subdomain can set or read it

The __Host- rule about the missing Domain attribute is the interesting one. Without Domain, a cookie is host-only, so staging.example.com cannot write a cookie that www.example.com will accept. That closes cookie tossing, the attack where a forgotten subdomain (an abandoned marketing site, a takeover-able CNAME, a shared hosting neighbour) writes a session or CSRF-token cookie for the whole registrable domain and the main app happily reads it. Since SameSite treats all of those subdomains as the same site, SameSite gives you no protection here at all. The prefix does.

Cost of adoption: rename session to __Host-session. Browsers have supported the prefixes since 2016 (Chrome 49, Firefox 50) and anything that does not know them just sees a cookie with a strange name. The one real constraint is that __Host- and Domain are mutually exclusive, so a cookie deliberately shared across subdomains has to settle for __Secure-.

Why SameSite alone is not CSRF protection

Lax-by-default killed the drive-by CSRF proof-of-concept, and that is genuinely a big win. It did not make CSRF tokens obsolete, for four concrete reasons.

  1. GET still carries Lax cookies. Any endpoint that changes state on a top-level GET, and every codebase has one, is reachable cross-site with full session cookies.
  2. Same-site is a wide net. Every subdomain of your registrable domain is same-site. An XSS or a subdomain takeover anywhere in that space gets full-strength cookies.
  3. The 120-second window. A cross-site POST right after login still gets the cookie, which is exactly the moment a session is most valuable.
  4. The attribute is advice to browsers. Anything that is not a browser ignores it completely.

Keep a CSRF token or a server-side Origin header check, and treat SameSite as the layer underneath. And do not expect CORS to fill the gap: CORS controls whether JavaScript may read a cross-origin response, it does not stop the request from being sent and executed, which is the point we hammer in CORS errors explained.

Cookies that stopped arriving

What does SameSite=Lax actually do?

SameSite=Lax sends the cookie on top-level navigations that use a safe method, and nothing else. Click a link from another site to yours, the cookie travels. Load your site in an iframe, request an image, run a fetch() call or submit a cross-site POST form, and it does not. Since Chrome 80 in February 2020, a cookie set without any SameSite attribute is treated as Lax by default, which is why so many embedded widgets and SSO flows broke that year.

What is the difference between SameSite=Lax and SameSite=Strict?

Strict withholds the cookie even when the user clicks a plain link from another site to yours, Lax sends it. The practical effect of Strict is that a user arriving from Slack, Google or an email sees the logged-out version of your page and has to reload to appear logged in. A common pattern is two cookies: a Lax one that proves identity for read-only rendering, and a Strict one that any state-changing request additionally requires.

Why is my SameSite=None cookie being rejected?

Because SameSite=None requires the Secure attribute and an HTTPS connection. Browsers reject the whole cookie rather than downgrading it, so it never gets stored at all and you see no cookie instead of a wrongly scoped one. Chrome makes an exception for http://localhost so local development keeps working. The other frequent cause is an old client (iOS 12, macOS 10.14 and some Java and .NET stacks) that does not understand the value None and treats it like Strict.

Are subdomains considered same-site?

Yes. SameSite works on registrable domains (eTLD+1 from the Public Suffix List), not origins, so app.example.com and staging.example.com are the same site and cookies flow between them freely. That is the reason a takeover of any forgotten subdomain is a session-level problem, not a cosmetic one. Ports do not matter either. Since Chrome 89, the scheme does: http and https count as different sites, a rule known as schemeful same-site.

What is a partitioned cookie (CHIPS)?

A partitioned cookie carries the Partitioned attribute and gets its own storage jar per top-level site, so the same third-party embed on shop-a.com and shop-b.com sees two entirely separate cookies. CHIPS (Cookies Having Independent Partitioned State) exists for embeds that need state, like a support chat widget or a payment iframe, without the cross-site tracking that made third-party cookies toxic. Partitioned cookies must also be Secure, and the spec recommends pairing them with the __Host- name prefix.

What does the __Host- cookie prefix do?

A cookie named with the __Host- prefix is only accepted by the browser if it has the Secure attribute, has Path=/, has no Domain attribute, and was set over HTTPS. The payoff is that no subdomain can set or overwrite it, which shuts down cookie tossing, where an attacker with control over one sibling subdomain writes a session cookie for the whole registrable domain. It costs nothing but a rename, and browsers that do not know the prefix simply see a cookie with an odd name.

Does SameSite protect against CSRF?

It removes most of the easy attacks and none of the interesting ones. Lax still sends cookies on top-level GET navigations, so any endpoint that changes state on a GET is unprotected. Sibling subdomains count as same-site, Chrome still has a two-minute tolerance window for fresh cookies on cross-site POSTs, and non-browser clients ignore the attribute entirely. Treat SameSite as defence in depth on top of CSRF tokens or a server-side Origin header check, not as a replacement.

Are third-party cookies still going away in Chrome?

No. Google abandoned the phaseout in July 2024 after four years of delays, confirmed in April 2025 that there would be no user prompt either, and in October 2025 retired the Privacy Sandbox advertising APIs (Topics, Protected Audience, Attribution Reporting), with the removal rolling through Chrome 144 in January 2026 to Chrome 150 in July 2026. Third-party cookies stay in Chrome with no end date. Safari and Firefox still block them by default, so building on them remains a bad bet.