Backend Systems · Session and State

Cookie Scope, Attributes, and Transport Rules

The exact matching rules that decide whether a browser attaches a session cookie to a request, and how Domain, Path, Secure, HttpOnly, and SameSite narrow that decision.

A cookie is a name-value pair plus a set of scope attributes, stored by the browser and replayed on later requests. The server that sets a cookie does not decide when it comes back; the browser does, using matching rules fixed by RFC 6265 and refined by its successor drafts. For a session cookie those rules are the entire delivery mechanism, so a mismatch between what the origin believes it set and what the browser actually stored is indistinguishable, from the server’s side, from a user who never logged in.

The scope is a pair of matches, not a prefix test

Two independent predicates must both pass before a cookie is attached to a request. The domain predicate compares the cookie’s Domain attribute with the request host. If Domain is absent, the cookie is host-only: it matches only the exact host that set it, and www.example.com and api.example.com are different hosts for this purpose. If Domain is present, the request host must domain-match it, meaning the host equals the attribute value or ends with it preceded by a dot. Setting Domain=example.com from www.example.com therefore makes the cookie available to every subdomain, and that widening is permanent until the cookie expires or is overwritten.

A leading dot in the Domain value is ignored by browsers even though the grammar in RFC 6265 does not permit it. The dot was meaningful in the obsolete Netscape specification and in RFC 2109, where it distinguished domain cookies from host-only ones; the modern rule is that presence of the attribute, not its punctuation, controls the distinction. Code that emits .example.com works, but the intent is clearer without it.

The path predicate is a prefix match on the request path with a boundary condition: the cookie path must equal the request path, or be a prefix of it ending in /, or be a prefix whose next character in the request path is /. A cookie with Path=/admin is sent to /admin, /admin/, and /admin/users, but not to /administrator. When Path is omitted, the browser derives a default from the request URI by taking everything up to but not including the rightmost /, so a response to /app/login yields Path=/app.

Ports play no part. A cookie set by https://example.com:8443 is sent to https://example.com:443 and to plain HTTP on any port, subject only to the Secure attribute. Applications that run several services on one hostname and expect path scoping to separate them are relying on a boundary the browser enforces only for sending, not for isolation: a service at /app can set Path=/ and shadow everything.

The attributes that constrain sending

Secure restricts the cookie to secure channels. It does not make the value confidential in any cryptographic sense; it only removes the plaintext-HTTP path from the set of requests that carry it. A single request over HTTP to the same host, before a redirect to HTTPS, would otherwise expose the identifier to anyone on the path.

HttpOnly removes the cookie from document.cookie and from the XMLHttpRequest and fetch header-building surfaces, which makes exfiltration through injected script require an actual request rather than a string read. It offers nothing against a network attacker, nothing against a browser extension with host permissions, and nothing against a same-site request forgery that the browser performs automatically.

SameSite governs cross-site sending and takes three values. Strict withholds the cookie on any request whose initiator is a different site, including top-level navigation from an external link, so a user arriving from a search result or an email appears logged out on the first request. Lax sends the cookie for top-level, safe-method navigations and withholds it for cross-site subresource requests and cross-site form submissions. None sends it in all cross-site contexts and, in current browsers, must be accompanied by Secure or the cookie is rejected.

The site boundary in SameSite is registrable domain plus scheme, not origin, so app.example.com and api.example.com are the same site for these purposes even though they are different origins. Defaults have moved: browsers treat a cookie with no SameSite attribute as Lax, and some browsers still send a newly set cookie on a cross-site top-level POST for a short window after it is set, a compatibility measure rather than a guarantee. An explicit attribute is the only way to be certain which behavior applies.

Host prefixes add a constraint the origin cannot talk its way out of. A cookie whose name begins __Host- is accepted only if it was set with Secure, with Path=/, and without Domain, which pins it to the exact host. A __Secure- prefix requires Secure alone. Both exist because a compromised or careless sibling subdomain can otherwise write a cookie with the same name and a wider Domain, producing two cookies of the same name in one request — the “cookie tossing” pattern — where the browser’s ordering rules decide which value arrives first and the server has no reliable way to tell them apart.

Max-Age is a relative lifetime in seconds; Expires is an absolute date. When both are present, Max-Age wins. A Max-Age of zero or a negative value, or an Expires in the past, deletes the cookie rather than storing it. Date parsing in Expires follows an old, permissive set of formats and is a recurring source of silent failure: the specification says a value that fails to parse causes the whole attribute to be ignored, so the cookie quietly becomes a session cookie instead of an expiring one.

A cookie with neither attribute is a session cookie. It lives in browser memory and is discarded when the browsing session ends, except that browsers offering session restore commonly persist such cookies across restarts, so “session” describes an intention rather than an enforced lifetime. Applications that require a hard stop need an explicit Max-Age and a matching server-side expiry, since the client-side value is advisory in every case. The relationship between cookie lifetime and record lifetime is the subject of How a Session Identifier Binds Requests to Server State.

The ceilings are worth knowing because they fail silently. RFC 6265 requires user agents to support at least 4096 bytes per cookie including name, value, and attributes, at least 50 cookies per domain, and at least 3000 cookies overall. Exceeding a per-cookie size limit causes the cookie to be dropped, not trimmed, and the request proceeds without it. Exceeding a count limit evicts the least recently used cookie, which may be the session cookie of an unrelated application on the same domain. A response that sets 60 cookies on a domain where the browser holds 50 will appear to succeed and will lose some of them.

When two cookies share a name

Browsers may hold several cookies with the same name for one host, differing in path or in the domain they were set from. RFC 6265 recommends the order in which they are serialized into the Cookie header: cookies with longer paths come first, and among cookies with equal path length, the one created earlier comes first. The specification warns that not every user agent sorts this way, so a server that parses the header with a naive split and takes the last value is reading the shortest-path cookie on the agents that do, which is usually the one set by the widest scope.

That ordering rule is the mechanism behind both cookie tossing and a mundane deployment bug. If an application is moved from /legacy to / and sets the same cookie name on both paths, requests to /legacy/... carry both values, and which one the framework keeps depends on the parser. Removing the stale cookie requires setting it again with the same name, path, and domain and an expiry in the past; deleting with a different path creates a second cookie and leaves the first in place.

Writes have the same composite-key semantics: setting a cookie replaces an existing one only when name, domain, and path all coincide. A login endpoint that sets Path=/ on one code path and relies on the default path on another produces two session cookies that coexist and take turns being sent depending on the request path. This is a common cause of intermittent authentication failures confined to a URL subtree.

Public suffixes are rejected as Domain values unless they happen to equal the request host, so Domain=co.uk is discarded. Without that rule any site under a shared suffix could set a cookie visible to all its neighbours. It also means a service hosted on a platform domain cannot set a parent-scoped cookie, which is by design and is not negotiable through headers.

Set-Cookie is not a list-valued header in the usual sense; it cannot be folded into a single comma-separated line, because Expires values contain commas. RFC 6265 defines it as a header that appears once per cookie, and implementations that join multiple Set-Cookie fields before parsing will split an Expires date in half. The presence of Set-Cookie does not by itself prevent a shared cache from storing and reusing a response, and replaying one visitor’s login response to another delivers a working identifier. RFC 9111’s private and no-store directives are the tools that prevent it, and a login response should carry one of them.

Attributes the browser does not recognize are simply ignored, which makes typos invisible: SameSite=Stric, Http-only, and Secur are all discarded, leaving a cookie without the restriction that was meant to apply. Case is not the hazard — attribute names match case-insensitively, so Httponly still works — and the valueless attributes discard whatever value they are given, so Secure=1 still applies Secure. The safe posture is to emit the canonical spelling and to verify behavior against a real client rather than against the framework’s configuration documentation.

References

  1. RFC 6265: HTTP State Management Mechanism — IETF
  2. draft-ietf-httpbis-rfc6265bis: Cookies: HTTP State Management Mechanism — IETF HTTP Working Group
  3. OWASP Session Management Cheat Sheet — OWASP Foundation