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
| 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 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).
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 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.
Securekeeps 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.
HttpOnlystops 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.
SameSitestrips 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=NonewithoutSecurenever 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
SameSiteattribute 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 explicitSameSite=Laxnever gets the exception, one more reason to set the attribute yourself. SameSiteis site-scoped, not origin-scoped. Sibling subdomains count as same-site, so the attribute gives no protection against a compromised subdomain. Andhttp://vshttps://on the same host counts as cross-site only where schemeful same-site shipped (Chrome, not Safari, Firefox behind a flag).Domainwidens exposure.Domain=example.comsends 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.
Strictdrops sessions on inbound links. ASameSite=Strictsession 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
- Name the session cookie with the
__Host-prefix and setSecure,HttpOnly, andSameSiteon it,Strictif your users never enter the app through cross-site links, otherwiseLax. - Drop the
Domainattribute and setPath=/;__Host-requires both, and host-only cookies are the point. - Omit
ExpiresandMax-Ageso the session cookie is non-persistent and dies with the browser session. - Test the cross-site entry flows (links from email, OAuth redirects, any
embedded content) before settling on
Strict, and give genuinely cross-site embed cookiesSameSite=None; Secure; Partitionedinstead of loosening the session cookie. - Check the deployed
Set-Cookieattributes, 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.
Why is my SameSite=None 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?
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
- Strict-Transport-Security,
the header that makes the
Secureattribute airtight by removing the plain HTTP hop entirely - Security headers overview
- Cache-Control to keep the responses that carry the session out of shared caches
- Security headers scanner to check your cookie attributes and the rest of your deployed headers
Sources
Strict-Transport-Security
The HSTS header pins HTTPS for future visits so the browser never retries plain HTTP. What it protects against and how to deploy it safely.
X-Content-Type-Options
The nosniff header stops browsers from second-guessing Content-Type, closing MIME-sniffing attacks. What it does and how to use it.