Cookies and Their Attributes
Six attributes decide whether a cookie is a reasonable place to keep a session or a liability: Secure, HttpOnly, SameSite, Domain, Path and lifetime — and each one maps to a specific attack.
Frame the problem
Security starts with a concrete asset, attacker capability and trust crossing.
Each attribute stops one specific thing
Cookie attributes are not a style checklist; each one exists because of a concrete attack, and knowing which attack tells you when an attribute is negotiable and when it is not.
`Secure` — send only over HTTPS. Without it a single plaintext request to the domain (a typed URL, an old bookmark, a hardcoded http:// link, an image) transmits the session id in the clear. Non-negotiable.
`HttpOnly` — hide the cookie from JavaScript. This is depth against XSS: an injected script can still *act* as the user by making requests, but it cannot read the token and send it somewhere for later use. It converts an unbounded compromise into one that lasts only as long as the script runs. Non-negotiable for session cookies.
`SameSite` — control whether the browser attaches the cookie to cross-site requests. Strict withholds it on every cross-site request, including top-level navigation, so following a link from an email lands you logged out. Lax withholds it on cross-site subresource requests and form POSTs but keeps it for top-level GET navigation, which is the usual right default. None sends it always and requires Secure; it is needed for genuine third-party embedding and removes the protection.
`Domain` — which hosts receive the cookie. Setting Domain=example.com sends it to every subdomain, including blog.example.com running unmaintained software and preview-x.example.com created by a build system. Omit Domain entirely to scope it to the exact host, which is what you want unless you specifically need sharing.
`Path` and lifetime — narrower is better, though Path is weak isolation since same-origin scripts can read across paths. A session cookie without Max-Age/Expires ends with the browser session, which is a reasonable default for high-value applications.
GOOD
Set-Cookie: sid=8Fq2...9xK; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=43200
└ host-only (no Domain=), script-invisible, HTTPS-only, not sent on cross-site POST
BAD
Set-Cookie: sid=8Fq2...9xK; Domain=.example.com; Path=/; Expires=Fri, 31 Dec 2027
│ └ two-year bearer token
└ every subdomain, including the WordPress blog nobody has patched
(no Secure → leaks on any http:// request)
(no HttpOnly → any XSS exfiltrates it)
(no SameSite → attached to cross-site POSTs; classic CSRF)
PREFIX (belt and braces for high-value sessions)
Set-Cookie: __Host-sid=...; HttpOnly; Secure; SameSite=Lax; Path=/
└ browser REFUSES this name unless Secure, Path=/ and no Domain —
so a compromised subdomain cannot overwrite itSubdomains are the quiet problem
Cookie scoping does not follow the same-origin policy, and this asymmetry is the source of a whole family of incidents. A cookie set with Domain=example.com is sent to every subdomain. Worse, a cookie *set by* any subdomain can be scoped to the parent domain, which means blog.example.com can write a cookie that app.example.com will receive.
That gives a compromised or merely careless subdomain two capabilities. It can read your session cookie if the cookie is domain-scoped — so an XSS on a marketing page reaches the application session. And it can write a cookie with the same name, which the application may accept, enabling session fixation from a host you do not think of as part of the application.
The defenses are specific. Omit Domain so cookies are host-only. Use the __Host- prefix for session cookies, which makes the browser refuse the cookie unless it is Secure, Path=/ and host-only — so no subdomain can overwrite it. Keep user-generated content and third-party-managed sites on a completely separate registrable domain, not a subdomain, so the cookie boundary and the trust boundary coincide.
| Attribute | Without it | Notes |
|---|---|---|
Secure | Session id transmitted in cleartext on any http:// request | Pair with HSTS so there is no plaintext request at all |
HttpOnly | Any XSS reads and exfiltrates the session | Depth, not prevention: the script can still act in-page |
SameSite=Lax | Cookie attached to cross-site form POSTs — CSRF | Defence in depth alongside CSRF tokens, not a replacement |
host-only (no Domain) | Every subdomain receives the session cookie | Blog, docs, preview environments all become session-adjacent |
__Host- prefix | A subdomain can overwrite the session cookie | Browser-enforced; costs nothing |
| Short lifetime | A stolen cookie is valid for months | Must be paired with server-side expiry; the client can lie |
Attributes are client-side hints, not server-side controls
Every attribute here is an instruction to a cooperating browser. A non-browser client — a script, a proxy, an attacker with the cookie value copied out of a log — ignores all of them. HttpOnly does not stop an attacker who already has the value; Max-Age does not stop a cookie whose value was saved last month.
So the server must enforce the properties it actually cares about independently. Expiry must be checked server-side against the session record, because a client that keeps sending an expired cookie is a client, not an error. Revocation must delete server state, because there is no way to make a browser forget a value it has already exfiltrated. And the session id must be rotated on privilege change, because attributes cannot express "this cookie is no longer the right one".
The attributes reduce the ways a cookie *escapes*; the server-side session record is what limits what an escaped cookie can do. Both halves are required, and teams that set the attributes carefully and then implement expiry only in Max-Age have built the first half twice.
Key points
HttpOnly; Secure; SameSite=Lax, host-only, short-lived is the default correct session cookie.- Omit
Domainand use the__Host-prefix so no subdomain can read or overwrite the session cookie. SameSiteis depth against CSRF, not a replacement for tokens on state-changing requests.- Cookie scoping does not follow the same-origin policy — a sibling subdomain is inside your cookie boundary.
- All attributes are browser-side hints; expiry and revocation must also be enforced against server-side state.
Boundary control exercise
This lesson uses the shared boundary-control exercise.
Follow the attack
Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.
- 1Attacker → find a cookie missing an attribute: no `Secure`, no `HttpOnly`, domain-scoped, or `SameSite=None`.
- 2Missing attribute → matching attack: sniff a plaintext request, read via XSS, ride via cross-site POST, or read from a sibling subdomain.
- 3Cookie value → replay from anywhere, with all attributes now irrelevant.
- Session theft with the full impact of impersonation, from a one-line configuration omission.
- Domain-scoped cookies expand the blast radius of any subdomain compromise to the main application.
- Long-lived cookies extend a single theft into months of access.
Defend, detect, recover
One prevention is a single point of security failure. Layer it and make failure observable.
- • Set attributes centrally in a session helper so no handler writes `Set-Cookie` by hand.
- • Enable HSTS with a long max-age and preload, so `Secure` is never tested by a plaintext request.
- • Host untrusted or third-party-managed content on a separate registrable domain, not a subdomain.
- • Add an automated check that fails CI when a response sets a session cookie without the required attributes.
- • Scan production responses for `Set-Cookie` headers lacking required attributes — cheap, and catches regressions immediately.
- • Alert on session cookies with a `Domain` attribute, which should be zero in a correct configuration.
- • Monitor for session cookies appearing in access logs or referrer headers, which indicates a leak path.
- • Fix the attributes and revoke all sessions issued under the incorrect configuration — the cookies already sent cannot be retracted.
- • If cookies were domain-scoped, treat every subdomain as having had session access for the whole period.
- • Third-party scripts on the page run with full origin privileges and can act within the session even with `HttpOnly`.
- • `SameSite` behaviour varies across browser versions and has defaults that have changed over time.
- • Legacy clients and embedded webviews may not honour prefixes or `SameSite` correctly.