SessionscookiesHttpOnlySecureSameSitedomainpath

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.

▶ Run the labFollow the failure

Frame the problem

Security starts with a concrete asset, attacker capability and trust crossing.

Asset
The session identifier stored in the browser, and the set of requests on which the browser will attach it.
Attacker & capability
A script injected into your page, a network observer on a plaintext hop, a different site making requests to yours, and a compromised sibling subdomain.
Trust boundary
The browser's origin and site model, which is the only thing deciding who can read the cookie and which requests carry it.
AssetThreatAttack SurfaceTrust BoundaryVulnerabilityExploit PathImpactMitigationDefense in DepthResidual Risk

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.

The cookie you want, and the one that shows up in incidents
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 it

Subdomains 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 → attack it addresses
AttributeWithout itNotes
SecureSession id transmitted in cleartext on any http:// requestPair with HSTS so there is no plaintext request at all
HttpOnlyAny XSS reads and exfiltrates the sessionDepth, not prevention: the script can still act in-page
SameSite=LaxCookie attached to cross-site form POSTs — CSRFDefence in depth alongside CSRF tokens, not a replacement
host-only (no Domain)Every subdomain receives the session cookieBlog, docs, preview environments all become session-adjacent
__Host- prefixA subdomain can overwrite the session cookieBrowser-enforced; costs nothing
Short lifetimeA stolen cookie is valid for monthsMust 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 Domain and use the __Host- prefix so no subdomain can read or overwrite the session cookie.
  • SameSite is 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.

Boundary control check
Untrusted input / identity
Trust boundary
Privileged asset
Prevention may fail silently.

Follow the attack

Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.

  1. 1
    Attacker → find a cookie missing an attribute: no `Secure`, no `HttpOnly`, domain-scoped, or `SameSite=None`.
  2. 2
    Missing attribute → matching attack: sniff a plaintext request, read via XSS, ride via cross-site POST, or read from a sibling subdomain.
  3. 3
    Cookie value → replay from anywhere, with all attributes now irrelevant.
Blast radius
  • 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.

Prevent
  • • 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.
Detect
  • • 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.
Respond & recover
  • • 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.
Residual risk
  • • 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.

Misconceptions

Claim
“`HttpOnly` protects against XSS.”
Reality
It prevents the script from *reading* the cookie. The script still runs on your origin and can make authenticated requests, so it can act as the user — it just cannot take the token away.
Claim
“`SameSite=Strict` is always better.”
Reality
It logs users out when they arrive from an external link, which teams then fix by lengthening sessions or adding a second cookie. `Lax` is usually the right default.