CentralCSP
Security headers

Cookie security

The Set-Cookie attributes that protect sessions, Secure, HttpOnly, SameSite, and the __Host- and __Secure- prefixes, explained simply.

Last update:

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:

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

Values and what each does

AttributeStatusDescription
Secure✅ GoodSend the cookie over HTTPS only, never on a plain HTTP request. Mandatory for session cookies.
HttpOnly✅ GoodHide the cookie from JavaScript. Scripts cannot read or exfiltrate the value.
SameSite=Strict✅ GoodSend only on same-site requests. Strongest CSRF protection, but drops the session on inbound cross-site links.
SameSite=Lax✅ GoodSame-site requests plus cross-site top-level navigations with safe methods (link clicks). The practical default for sessions.
SameSite=None❌ RiskySend on all cross-site requests. Legitimate only for cross-site embeds, requires Secure, and Partitioned is the better fit for widget state.
(no SameSite)❌ RiskyThe 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✅ GoodThe browser rejects the cookie unless it has Secure, no Domain, and Path=/. Binds it to exactly one host.
__Secure- prefix✅ GoodThe browser rejects the cookie unless it is set with Secure from a secure origin.
__Http- / __Host-Http- prefixes🧪 ExperimentalAdditionally require HttpOnly, proving the cookie came from a header and never from script. In Chrome and Firefox, not Safari, being standardized.
Partitioned✅ GoodStores 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 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 so the insecure request never leaves the machine in the first place.

HttpOnly

HttpOnly hides the cookie from document.cookie and the 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 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).

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 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

  • 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 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 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 turned into real session-fixation attacks. The __Host- prefix closes 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

  • 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 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

  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.

Recommendation

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

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

This follows the OWASP Session Management cheat sheet: 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

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 while retaining CHIPS, Safari blocks them with Intelligent Tracking Prevention, and Firefox partitions them with Total Cookie Protection.

FAQ

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 covers the gaps and the documented bypasses in detail.

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.

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?

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

Sources

On this page