# Cookie security (/en/docs/web-security/security-headers/cookie-security)



Cookies carry the session. After login the server sets a cookie, the browser
attaches it to every matching request, and whoever holds that cookie value is
logged in as the user. There is no single "cookie security" header; the
protection lives in a handful of attributes on the `Set-Cookie` response
header itself.

Those attributes (`Secure`, `HttpOnly`, `SameSite`, the `__Host-` and
`__Secure-` name prefixes, and `Partitioned`) decide whether the cookie can be
read off the network, stolen by injected script, replayed by a cross-site
request, or planted by a subdomain. Each one is a few characters on the same
line, and leaving them off is what makes a session stealable.

This is a hardened session cookie as you would ship it:

```http
Set-Cookie: __Host-session=<value>; Secure; HttpOnly; SameSite=Lax; Path=/
```

## Values and what each does [#values-and-what-each-does]

| Attribute                           | Status          | Description                                                                                                                                                |
| ----------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Secure`                            | ✅ Good          | Send the cookie over HTTPS only, never on a plain HTTP request. Mandatory for session cookies.                                                             |
| `HttpOnly`                          | ✅ Good          | Hide the cookie from JavaScript. Scripts cannot read or exfiltrate the value.                                                                              |
| `SameSite=Strict`                   | ✅ Good          | Send only on same-site requests. Strongest CSRF protection, but drops the session on inbound cross-site links.                                             |
| `SameSite=Lax`                      | ✅ Good          | Same-site requests plus cross-site top-level navigations with safe methods (link clicks). The practical default for sessions.                              |
| `SameSite=None`                     | ❌ Risky         | Send on all cross-site requests. Legitimate only for cross-site embeds, requires `Secure`, and `Partitioned` is the better fit for widget state.           |
| (no `SameSite`)                     | ❌ Risky         | The default varies by browser, and Chrome's default-Lax keeps a 2-minute exception for cross-site POSTs. Always set the attribute explicitly.              |
| `__Host-` prefix                    | ✅ Good          | The browser rejects the cookie unless it has `Secure`, no `Domain`, and `Path=/`. Binds it to exactly one host.                                            |
| `__Secure-` prefix                  | ✅ Good          | The browser rejects the cookie unless it is set with `Secure` from a secure origin.                                                                        |
| `__Http-` / `__Host-Http-` prefixes | 🧪 Experimental | Additionally require `HttpOnly`, proving the cookie came from a header and never from script. In Chrome and Firefox, not Safari, being standardized.       |
| `Partitioned`                       | ✅ Good          | Stores a cross-site cookie per top-level site (CHIPS) so an embed keeps state without becoming a cross-site identifier. Baseline widely available per MDN. |

### Secure [#secure]

`Secure` tells the browser to attach the cookie only to HTTPS requests, so it
never crosses the network in cleartext (browsers waive the requirement on
localhost). It restricts transmission, not setting: an attacker answering a
plain HTTP request can still set cookies for the host, although a non-secure
`Set-Cookie` cannot overwrite an existing `Secure` cookie with the same name.
Pair it with
[Strict-Transport-Security](/en/docs/web-security/security-headers/strict-transport-security)
so the insecure request never leaves the machine in the first place.

### HttpOnly [#httponly]

`HttpOnly` hides the cookie from `document.cookie` and the
[Cookie Store API](https://developer.mozilla.org/en-US/docs/Web/API/Cookie_Store_API),
so a script running in the page cannot read the value. The browser still
attaches the cookie to the requests that scripts trigger, which means injected
code can act as the user in place; what `HttpOnly` prevents is copying the
session out for later use. Set it on every cookie JavaScript does not need,
which for session cookies is all of them.

### SameSite [#samesite]

`SameSite` controls whether the cookie rides cross-site requests. `Strict`
sends it on same-site requests only. `Lax` adds cross-site top-level
navigations with safe methods (a clicked link, a GET form), but never
cross-site subresources such as `fetch()` calls, images, or iframes. `None`
sends it everywhere and is rejected outright unless `Secure` is also set.
Never rely on the browser default; it differs per browser (see the browser
support section below).

### Cookie prefixes [#cookie-prefixes]

The prefixes are naming conventions the browser enforces when the cookie is
set. A `__Secure-` cookie must carry `Secure` and come from a secure origin. A
`__Host-` cookie must also have no `Domain` attribute and `Path=/`, which
binds it to exactly one host: subdomains cannot set or shadow it, closing the
cookie-tossing hole described below. The newer `__Http-` and `__Host-Http-`
prefixes (supported in Chrome and Firefox, being standardized) additionally
require `HttpOnly`, so the name itself proves the cookie was set by a header
and never by script.

### Partitioned [#partitioned]

`Partitioned` opts a cross-site cookie into storage keyed by the top-level
site (the CHIPS proposal), so an embedded widget keeps separate state on every
site that embeds it instead of one identifier that follows the user across the
web. It requires `Secure` and is used together with `SameSite=None`, and MDN
recommends setting it with the `__Host-` prefix. It reached Baseline when
Firefox and Safari joined Chrome in supporting it.

## What it protects against [#what-it-protects-against]

* **Session interception in transit.** `Secure` keeps the cookie off plain
  HTTP hops, where a passive eavesdropper on the path could copy it and
  replay the session. OWASP treats it as mandatory for session cookies.
* **Cookie theft through XSS.** `HttpOnly` stops an injected script from
  reading the session value and shipping it to an attacker's server. It does
  not stop the script from using the session in place, because the browser
  still attaches the cookie to requests the page makes; a
  [Content Security Policy](/en/docs/web-security/policies/content-security-policy)
  addresses the injection itself.
* **Cross-site request forgery, as defense in depth.** `SameSite` strips the
  cookie from most cross-site requests, which is what CSRF abuses. It narrows
  the attack, it does not replace CSRF tokens (see the gotchas below for why,
  and [SameSite vs CSRF tokens](/en/blog/samesite-vs-csrf-tokens) for the full
  comparison).
* **Cookie tossing from subdomains.** Any subdomain can normally plant a
  cookie the parent application will trust, a weak-integrity problem
  rfc6265bis documents and
  [Snyk's OAuth research](https://labs.snyk.io/resources/hijacking-oauth-flows-via-cookie-tossing/)
  turned into real session-fixation attacks. The `__Host-` prefix closes it.

## Risks without it [#risks-without-it]

A session cookie with no attributes is exposed on every front. It travels in
cleartext on any plain HTTP request to the host, where anyone on the network
path can copy it and hijack the session. Any script injected into the page can
read it through `document.cookie` and exfiltrate it. Every cross-site request
carries it, which is exactly the opening CSRF needs. And any subdomain,
including a forgotten or vulnerable one, can plant a look-alike cookie the
main application will accept, fixing the victim into an attacker-controlled
session.

## Risks and gotchas when using it [#risks-and-gotchas-when-using-it]

* **`SameSite=None` without `Secure` never lands.** Chrome, Edge, and current
  Firefox silently refuse to store the cookie. This is the top cause of
  "my cross-site cookie disappeared".
* **Chrome's 2-minute Lax+POST exception.** Cookies with no `SameSite`
  attribute are still sent on cross-site top-level POSTs for 2 minutes after
  creation, an exception still applied by Chrome as of mid-2026 with no
  removal date announced. An explicit `SameSite=Lax` never gets the
  exception, one more reason to set the attribute yourself.
* **`SameSite` is site-scoped, not origin-scoped.** Sibling subdomains count
  as same-site, so the attribute gives no protection against a compromised
  subdomain. And `http://` vs `https://` on the same host counts as
  cross-site only where schemeful same-site shipped (Chrome, not Safari,
  Firefox behind a flag).
* **`Domain` widens exposure.** `Domain=example.com` sends the cookie to every
  subdomain and lets every subdomain overwrite it. Omit it so the cookie
  stays host-only; `__Host-` enforces exactly that.
* **Prefixes are client-side hardening only.** A browser that does not know a
  prefix stores the cookie without any checks, and
  [PortSwigger documented parser edge cases](https://portswigger.net/research/cookie-chaos-how-to-bypass-host-and-secure-cookie-prefixes)
  that bypass the checks in browsers that do. They harden a design, they do
  not replace server-side session controls.
* **`Strict` drops sessions on inbound links.** A `SameSite=Strict` session
  cookie is not sent when the user arrives from another site, so visitors
  following a link from email or search results look logged out until they
  navigate again.

## How to set it up [#how-to-set-it-up]

1. Name the session cookie with the `__Host-` prefix and set `Secure`,
   `HttpOnly`, and `SameSite` on it, `Strict` if your users never enter the
   app through cross-site links, otherwise `Lax`.
2. Drop the `Domain` attribute and set `Path=/`; `__Host-` requires both, and
   host-only cookies are the point.
3. Omit `Expires` and `Max-Age` so the session cookie is non-persistent and
   dies with the browser session.
4. Test the cross-site entry flows (links from email, OAuth redirects, any
   embedded content) before settling on `Strict`, and give genuinely
   cross-site embed cookies `SameSite=None; Secure; Partitioned` instead of
   loosening the session cookie.
5. Check the deployed `Set-Cookie` attributes, along with the rest of your
   response headers, with the
   [security headers scanner](/tools/security-headers).

## Recommendation [#recommendation]

Set every session cookie with the `__Host-` prefix plus `Secure`, `HttpOnly`,
and an explicit `SameSite` value:

```http
Set-Cookie: __Host-session=<value>; Secure; HttpOnly; SameSite=Lax; Path=/
```

This follows the
[OWASP Session Management cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html):
`Secure` and `HttpOnly` always, `SameSite=Strict` preferred or `Lax` where
cross-site entry links must keep the session, never the browser default, and
the `__Host-` prefix on session identifiers. No `Domain` attribute, a
restrictive `Path`, and no `Expires` or `Max-Age` so the cookie does not
persist on disk.

## Browser support [#browser-support]

The core attributes are universal: `Secure`, `HttpOnly`, all three `SameSite`
values, and the `__Secure-` and `__Host-` prefixes work in every current
browser. The differences are in the defaults and the newer attributes. Chrome
and Edge treat a cookie without `SameSite` as `Lax` (minus the 2-minute POST
exception above); Firefox has that default behind a flag only; Safari never
applies it, so an attribute-less cookie behaves like `None` there. The rule
that `SameSite=None` requires `Secure` is enforced by Chrome, Edge, and
current Firefox, but not by Safari. The `__Http-` and `__Host-Http-`
prefixes are new in Chrome and Firefox with no Safari support, and
`Partitioned` is Baseline widely available across Chrome, Firefox, and Safari.
The third-party cookie landscape settled in 2025: Chrome
[kept third-party cookies](https://privacysandbox.google.com/blog/update-on-plans-for-privacy-sandbox-technologies)
while retaining CHIPS, Safari blocks them with Intelligent Tracking
Prevention, and Firefox partitions them with Total Cookie Protection.

## FAQ [#faq]

### Does SameSite replace CSRF tokens? [#does-samesite-replace-csrf-tokens]

No. `SameSite` narrows CSRF as defense in depth but stays site-scoped and
browser-dependent, so keep your CSRF tokens.
[SameSite vs CSRF tokens](/en/blog/samesite-vs-csrf-tokens) covers the gaps
and the documented bypasses in detail.

### SameSite Strict or Lax? [#samesite-strict-or-lax]

`Strict` sends the cookie only on same-site requests, so it drops the session
when a user arrives from an inbound cross-site link and they look logged out
until they navigate again. `Lax` keeps top-level GET navigations, so sessions
survive links from email or search. OWASP prefers `Strict`; `Lax` is
acceptable.

### Why is my SameSite=None cookie not set? [#why-is-my-samesitenone-cookie-not-set]

It is missing the `Secure` attribute. A cookie set with `SameSite=None` is
rejected outright unless `Secure` is also present, so Chrome, Edge, and current
Firefox silently refuse to store it. Add `Secure`, and for embedded widget
state prefer `SameSite=None; Secure; Partitioned` so the cookie does not become
a cross-site identifier.

### Does HttpOnly stop CSRF? [#does-httponly-stop-csrf]

No. `HttpOnly` hides the cookie from `document.cookie` and the Cookie Store API,
which stops a script from reading and exfiltrating the value. It does not stop
the browser from attaching the cookie to cross-site requests, which is exactly
what CSRF abuses. Use `SameSite` and CSRF tokens for that.

## See also [#see-also]

* [Strict-Transport-Security](/en/docs/web-security/security-headers/strict-transport-security),
  the header that makes the `Secure` attribute airtight by removing the plain
  HTTP hop entirely
* [Security headers overview](/en/docs/web-security/security-headers)
* [Cache-Control](/en/docs/web-security/security-headers/cache-control) to
  keep the responses that carry the session out of shared caches
* [Security headers scanner](/tools/security-headers) to check your
  cookie attributes and the rest of your deployed headers

## Sources [#sources]

* [draft-ietf-httpbis-rfc6265bis, Cookies, HTTP State Management Mechanism](https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html)
* [MDN, Set-Cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie)
* [OWASP, Session Management cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)
* [MDN, Cookies having independent partitioned state (CHIPS)](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Third-party_cookies/Partitioned_cookies)
* [Chromium source, cookie\_constants.h (the Lax+POST exception)](https://github.com/chromium/chromium/blob/main/net/cookies/cookie_constants.h)
