# SameSite Cookie Attribute: Strict, Lax and None

> How SameSite works: Strict, Lax and None, what counts as same-site, why Lax-by-default is Chromium behavior, and how to fix cookies blocked cross-site.

Source: https://howhttpworks.com/cookies/same-site
Last reviewed: 2026-10-04

> **TL;DR:** `SameSite` decides whether a cookie rides along on requests that start from another site. Set it explicitly: `Lax` for session cookies, `Strict` for high-risk ones, `None; Secure` only for cookies that must work inside third-party embeds. The "unset means Lax" default is Chromium-only.

## Same-site is not same-origin

The most common SameSite confusion is the definition of "site". A site is the scheme plus the registrable domain (eTLD+1, computed with the Public Suffix List). Port and subdomain do not matter.

| Request from | To | Same-site? |
| --- | --- | --- |
| `https://app.example.com` | `https://api.example.com` | Yes (both `example.com`) |
| `https://example.com:3000` | `https://example.com:8443` | Yes (ports ignored) |
| `https://example.com` | `https://example.org` | No |
| `http://example.com` | `https://example.com` | No in Chrome and Edge (schemeful same-site, Chrome 89+); other browsers have not matched this consistently, so do not rely on it |
| `https://alice.github.io` | `https://bob.github.io` | No (`github.io` is on the Public Suffix List) |

So a frontend on `app.example.com` calling `api.example.com` sends `SameSite=Strict` cookies fine, while the same frontend calling `api.example.net` does not, even though CORS is configured correctly. CORS and SameSite are independent checks; see [CORS vs CSP](https://howhttpworks.com/compare/cors-vs-csp) and the [CORS guide](https://howhttpworks.com/guides/cors).

## The three values

| Value | Cross-site top-level link (GET) | Cross-site POST form | fetch / XHR / iframe / img |
| --- | --- | --- | --- |
| `Strict` | Not sent | Not sent | Not sent |
| `Lax` | Sent | Not sent | Not sent |
| `None` | Sent | Sent | Sent (requires `Secure`) |

```http
Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly; Path=/
```

`Strict` has a UX cost: a user who clicks a link to your site from an email or chat arrives without the cookie, so the first page renders logged out. Typical workaround is a `Lax` session cookie plus a `Strict` cookie only for sensitive actions, or a redirect hop that re-requests the page from same-site context.

## Browser differences

| Behavior | Chrome / Edge | Firefox | Safari |
| --- | --- | --- | --- |
| Missing `SameSite` treated as `Lax` | Yes (since Chrome 80) | No | No |
| "Lax+POST" exception for unmarked cookies younger than 2 minutes | Yes | Not applicable | Not applicable |
| `SameSite=None` requires `Secure` | Yes (enforced alongside Lax-by-default) | Not enforced by default; the requirement is in RFC 6265bis | Not enforced; Safari 12 and macOS 10.14's Safari 13 treat `None` as `Strict`, fixed in 10.15 and later |
| Third-party cookies | Still allowed by default as of this review; `Partitioned` (CHIPS) available | Blocked or partitioned by Total Cookie Protection | Blocked by Intelligent Tracking Prevention |

Treat the table as a snapshot: third-party cookie policy has changed repeatedly, and Firefox and Safari restrictions apply even to cookies with `SameSite=None`. The practical rule is the same everywhere: if your feature needs a cookie in a cross-site iframe, test in all three browsers and plan for `Partitioned` cookies or the Storage Access API instead.

The Lax+POST exception exists only in Chromium and only for cookies with no explicit SameSite attribute that were set less than two minutes ago. It was a migration aid for SSO flows that POST back to the relying party. Do not depend on it; set `SameSite=None; Secure` explicitly if you need cross-site POST.

## Console messages and what they mean

| Browser message | Cause |
| --- | --- |
| `Cookie "x" has been rejected because it is in a cross-site context and its "SameSite" is "Lax" or "Strict"` (Firefox) | A cross-site request tried to set or send a Lax/Strict cookie |
| `This Set-Cookie was blocked because it had the "SameSite=None" attribute but did not have the "Secure" attribute` (Chrome) | Add `Secure` and serve over HTTPS |
| `This attempt to set a cookie via a Set-Cookie header was blocked because it had the "SameSite=Strict" or "SameSite=Lax" attribute but came from a cross-site response` (Chrome) | Cross-site `fetch` response set a Lax/Strict cookie; use `None; Secure` or restructure |

Reproduce what the server sends before debugging the browser:

```bash
curl -sI https://example.com/login | grep -i '^set-cookie'
```

## Fix: cookie missing in an embed or cross-site API

```javascript
// Express: cross-site API called from another registrable domain
res.cookie('session', token, {
  sameSite: 'none',
  secure: true,
  httpOnly: true,
  partitioned: true // CHIPS: separate jar per top-level site; Chromium and Firefox
})
```

```python
# Django settings.py
SESSION_COOKIE_SAMESITE = 'None'
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'None'
CSRF_COOKIE_SECURE = True
```

```python
# Flask
app.config.update(SESSION_COOKIE_SAMESITE='None', SESSION_COOKIE_SECURE=True)
```

On the client the request must also opt in: `fetch(url, { credentials: 'include' })`, and the server must send `Access-Control-Allow-Credentials: true` with a specific (non-`*`) `Access-Control-Allow-Origin`.

## OAuth and SSO redirects

When an identity provider redirects back to your callback, that is a cross-site top-level GET navigation. A `Strict` session cookie is not sent, so the callback sees no session. `Lax` works for this flow. If the provider returns the result with a `form_post` response mode (cross-site POST), even `Lax` cookies are withheld; the state cookie for that callback needs `SameSite=None; Secure` with a short `Max-Age`.

## SameSite does not replace CSRF tokens

`Lax` still allows cross-site GET navigations, and requests from sibling subdomains such as `evil.example.com` to `app.example.com` are same-site. Keep CSRF tokens (or `Sec-Fetch-Site` checks) for state-changing endpoints, and never mutate state on GET.

## Related

- [HttpOnly cookie](https://howhttpworks.com/cookies/http-only)
- [Secure cookie](https://howhttpworks.com/cookies/secure)
- [Expires and Max-Age](https://howhttpworks.com/cookies/expires)
- [Set-Cookie header](https://howhttpworks.com/headers/set-cookie)
- [Cookie security guide](https://howhttpworks.com/guides/cookie-security)
