toolhq.io

All posts
8 min readby Jameel Haider

Cookie flags explained: SameSite, Secure, HttpOnly, and Domain

A cookie is a name, a value, and a set of attributes that decide when the browser will send it back. The attributes are where the security lives, and they are consistently the part people copy from an old snippet without reading. A cookie missing HttpOnly is readable by any script on the page. A cookie missing Secure can be sent in the clear. A cookie with the wrong SameSite either leaks across sites or silently breaks your login flow. Here is what each one actually does.

A complete Set-Cookie response header looks like this:

Set-Cookie: session=abc123; Path=/; Max-Age=3600; Secure; HttpOnly; SameSite=Lax

You can see the exact Set-Cookie headers any origin returns with the HTTP Headers tool.

What does each attribute do?

AttributeEffectRecommended
HttpOnlyHides the cookie from document.cookieAlways, for session cookies
SecureOnly sent over HTTPSAlways
SameSiteControls sending on cross site requestsLax, or None when genuinely needed
DomainWhich hosts receive itOmit unless you need subdomains
PathWhich paths receive it/
Max-Age / ExpiresLifetimeSet explicitly, short for sessions
PartitionedSeparate storage per top level siteFor embedded third party cookies

HttpOnly is the one that matters most and costs nothing. It does not stop cross site scripting, but it stops an injected script from reading the session token and posting it elsewhere. A session cookie without HttpOnly turns any XSS into full account takeover. Set it, and pair it with a Content Security Policy.

Secure means the cookie is only attached to HTTPS requests. It is now effectively mandatory: SameSite=None is rejected without it, and modern browsers refuse to let an insecure page overwrite a Secure cookie. Note the asymmetry that surprises people, a plain HTTP page on the same host can still set cookies that HTTPS requests will send, which is why Strict-Transport-Security belongs in the same conversation.

Path is a scoping convenience, not a security boundary. Scripts running under /app can reach cookies set on /admin in practice, because the browser's isolation unit is the origin, not the path.

SameSite: Lax, Strict, or None?

SameSite decides whether the cookie is attached when the request originates from a different site. This is the attribute that both prevents request forgery and breaks logins, depending on the value.

SameSite=Lax sends the cookie on top level navigations that use safe methods, meaning a user clicking a link from another site arrives logged in, but withholds it from cross site POSTs, iframes, and background fetches. This is the default in Chrome and Firefox when no SameSite is specified, and it is the right answer for most session cookies. It removes the classic cross site request forgery vector, where a hidden form on an attacker's page submits to your endpoint with the victim's cookies attached.

SameSite=Strict withholds the cookie on every cross site request including plain link clicks. Users following a link from an email or search result arrive logged out, then appear logged in after any in site navigation, which reads as a bug to them. Reserve it for genuinely sensitive cookies, or use the split cookie pattern: a Strict cookie for state changing actions plus a Lax cookie for read only session recognition.

SameSite=None; Secure attaches the cookie to every cross site request. It is required for legitimate embedded use cases: a widget in an iframe on someone else's domain, a checkout inside a partner site, cross domain single sign on. It is also the setting that browser privacy work has been steadily narrowing, so pair it with Partitioned (CHIPS) where the cookie only needs to work per embedding site. None without Secure is rejected outright.

One clarification that trips people up: "same site" is not "same origin". app.example.com and api.example.com are the same site, so requests between them are not cross site and Lax cookies flow normally. Only when the registrable domain differs does SameSite engage. That also means CORS and SameSite are separate systems; passing one does not imply the other. A cross origin fetch to a sibling subdomain still needs credentials: 'include' on the client and Access-Control-Allow-Credentials on the server, regardless of SameSite.

Domain scoping is wider than it looks

Omitting Domain produces a host only cookie: example.com sets it, only example.com receives it. Setting Domain=example.com widens it to every subdomain, including ones you did not intend, such as a marketing site on promo.example.com or a customer controlled *.example.com tenant. Anything on any of those subdomains can then read the cookie. Omit Domain unless you specifically need subdomain sharing.

The __Host- prefix enforces the tight version. A cookie named __Host-session is only accepted if it is Secure, has Path=/, and has no Domain attribute, which makes cookie fixation from a sibling subdomain impossible:

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

A sensible default

For a server side session cookie:

Set-Cookie: __Host-session=<random>; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1209600

The value itself should be a high entropy random identifier or a signed token, never a user ID and never anything predictable. Generate one with a secure token generator, and if you are considering storing a JWT here instead, the tradeoffs are laid out in where to store JWT tokens.

Finally, deleting a cookie means resending it with the same Name, Path, and Domain and an expiry in the past. Change any one of those three and the browser treats it as a different cookie, leaves the original in place, and your logout quietly does nothing. Confirm the result by inspecting the response headers with the HTTP Headers tool rather than trusting the code path.