Security Engineering · Authentication
How Cookie Attributes Bound a Session Credential
What the Domain, Path, Secure, HttpOnly, and SameSite attributes actually constrain, which of them are enforced by the browser versus the server, and where the scope model leaks.
A session cookie is a bearer credential with a delivery mechanism bolted on. The delivery mechanism is HTTP’s cookie header, and it is unusual among protocol features in that its scope is defined not by a single specification but by a set of attributes that different parts of the stack enforce at different times. The server decides what to send; the browser decides what to store and when to send it back; and the two decisions use different models of identity.
The scope tuple and who enforces it
A cookie is stored as a tuple of name, value, domain, path, and flags. When a browser is about to make a request, it scans its store for tuples that match the request’s host and path and whose flags permit the request’s context. That matching is the browser’s job and it happens on every request, which is why the attributes are worth understanding as constraints on the browser rather than as instructions to the server.
Domain and Path together define the set of URLs to which the credential will be attached. The Domain attribute, if present, must be a suffix of the request host, and the browser will then send the cookie to that domain and all of its subdomains. Omitting Domain yields a host-only cookie, sent only to the exact host that set it. Path matching is a prefix comparison on the request path with a boundary rule: a cookie with Path=/admin matches /admin and /admin/users but not /administrator, because the match must end at a path separator or the end of the string.
The prefix rule has a consequence that surprises people. A cookie set with Path=/admin is not protected from a page at /. The browser will refuse to send it to /, but the page at / can still read it if it is not HttpOnly, and it can overwrite it, because the path is a sending restriction and not a read or write restriction. Setting a cookie with a narrower path than the application’s root does not create a security boundary between parts of the same origin. Origin is the security boundary; path is not.
Domain is the attribute with the real footgun. A cookie set with Domain=example.com by www.example.com is sent to example.com and every subdomain, including ones the application does not control if any subdomain is delegated to a third party. A host-only cookie is strictly narrower. The defensive default is to omit Domain entirely unless a credential genuinely has to cross subdomains, and to keep session credentials host-only.
Secure, HttpOnly, and what they do not cover
Secure instructs the browser to send the cookie only over a channel the browser considers secure. In practice that means TLS, with one exception: browsers treat http://localhost as a trustworthy origin for development purposes, which is a source-level convention rather than a protocol property. The attribute does not encrypt the cookie, does not prevent the server from setting it over plaintext in a way that a network attacker can observe, and does not prevent a same-origin script from reading it.
That last point is the reason HttpOnly exists. It removes the cookie from the document’s document.cookie interface, so a script running in the page cannot read it. It does not prevent the script from causing requests that carry the cookie, which is what makes it a partial defence against cross-site scripting: an injected script cannot exfiltrate the session token directly, but it can act as the user for the lifetime of the page, and it can read anything the page can read. HttpOnly raises the cost of turning an XSS into a persistent account takeover, since the token has to be used in place rather than stolen and replayed later from elsewhere. It does not make XSS harmless.
The combination of the two attributes is the minimum for a session cookie: Secure keeps the token off plaintext channels, HttpOnly keeps it out of the DOM. Neither is a substitute for treating the token as a secret in the server logs, in error reports, and in any URL the application constructs.
The __Host- and __Secure- name prefixes are an additional mechanism worth knowing. A cookie whose name begins with __Host- is only accepted by the browser if it was set with Secure, with Path=/, and without a Domain attribute. A cookie beginning with __Secure- is only accepted if set with Secure. These are enforced by the browser at storage time, so a subdomain that manages to set a cookie named __Host-session cannot widen its scope or drop TLS; the cookie is rejected. The prefix turns a set of conventions into a constraint the browser checks, which is the strongest form of cookie hardening available, because it cannot be forgotten by a future code path that writes the header by hand.
SameSite and the request-context dimension
SameSite answers a different question from the other attributes. It does not constrain which URL receives the cookie; it constrains which context may cause the request. The three values are Strict, Lax, and None.
Strict withholds the cookie on any request the browser classifies as cross-site, including top-level navigations. A user following a link from an external site to a logged-in page arrives logged out, and the application sees an anonymous request until the next same-site navigation. Lax permits the cookie on top-level navigations that use safe methods, so following a link arrives logged in, while a cross-site form POST or a cross-site subresource request does not carry it. None removes the restriction entirely and requires Secure in current browser behaviour.
The classification of “site” is where the details bite. Site is computed from the registrable domain, which is the public suffix plus one label; a.example.com and b.example.com are the same site but different origins. Two different customers of a hosting platform on customer-a.example.net and customer-b.example.net are different sites. A request from https://example.com to https://example.com.evil.test is cross-site. The public suffix list is the input to this computation, and it changes over time, which means the same pair of hosts can change classification without either application changing.
Lax as a default is a browser policy, not a protocol guarantee. It was adopted because the alternative, sending cookies on all cross-site requests, made cross-site request forgery a default-on condition. Lax blocks the classic attack shapes: a cross-site form POST, a cross-site fetch, a cross-site image or script load. It does not block top-level GET navigations, so any state-changing operation reachable by GET remains forgeable. A server that performs a mutation on a GET has effectively opted out of the protection SameSite provides, which is why the safe-method distinction in HTTP is a security property and not a style preference.
SameSite is also not a complete CSRF defence on its own. Browser versions differ in how they handle requests that are same-site but cross-origin, and in how they treat requests from extensions or from contexts with relaxed settings. The layered answer is SameSite=Lax plus an explicit anti-CSRF token bound to the session, so that a request which carries the cookie still has to carry a value that a cross-site attacker cannot read. That token check is a comparison of a secret, and the mechanics of doing it without an oracle are covered in constant-time comparison.
Setting, clearing, and the overwrite problem
The Set-Cookie header has no facility for modifying an existing cookie other than overwriting it. A response can set a cookie with the same name, domain, and path, and the browser replaces the stored value. This is how logout works: the server sends an expired cookie with the same scope tuple, and the browser removes it.
The consequence is that the scope tuple, not the name, is the identity of the cookie. Two cookies can share a name and differ in path or domain, and both will be stored and sent on overlapping requests. A common bug is a session cookie set at Path=/ by the main application and a second cookie of the same name set at Path=/app by a legacy handler; the browser sends both, and the server’s parser picks one, usually the more specific path. This is a real mechanism for session confusion and it is invisible in the application code, which sees only the header it receives.
Clearing has the same property: a logout that expires the cookie at Path=/ does not remove a copy stored at Path=/app. Every path and domain under which the cookie was ever set has to be expired individually. A session store that records the scope it issued helps here, because otherwise the list of scopes to clear is the list of every code path that has ever written the header.
The Max-Age and Expires attributes set persistence. Omitting both yields a session cookie, which the browser discards when the session ends, a boundary the browser defines and the user can influence. A persistent cookie with Max-Age survives restarts and is the right choice for a “remember me” credential, which should be a separate token from the session and should be rotated on use. Neither attribute has any effect on the server’s own notion of session lifetime, which is the subject of session identifier rotation: the browser’s expiry is a convenience, and the server must still expire the session on its own schedule.
Size, count, and the header budget
Browsers enforce limits that shape design. A single cookie is capped at 4096 bytes including name, value, and attributes, and each domain also has a cap on the number of cookies it may store, in the low hundreds depending on the browser. Exceeding either limit causes the browser to drop cookies silently, which produces a session that works while the payload is small and fails once it grows.
Session identifiers should be nowhere near that limit. A 128-bit random identifier encodes to 22 base64url characters or 32 hexadecimal ones; anything much larger is carrying data rather than identifying a session. A cookie that carries claims, roles, or preferences is a token, not an identifier, and the trade-offs of that design are covered in opaque identifiers versus signed tokens.
The Cookie request header has no compression and is sent on every request, including every subresource. A site with a large cookie set pays that cost on each of them, which is a throughput argument for keeping the credential small and for scoping it as narrowly as the origin model permits.
Residual exposure
Even a cookie with every attribute set correctly is exposed to anything that can observe the plaintext request. Secure prevents transmission over an unencrypted channel the browser recognises, but a TLS-terminating proxy that logs full headers writes the token to disk, and an error-tracking service that captures request headers ships it to a third party. The attributes constrain the browser; they do not constrain the server’s own handling.
The same applies to the lifetime of the value. A cookie whose token never changes is a credential that, once captured, works until the server expires it. Rotation at privilege changes and at login is the control that limits that window, and it is a server-side property that no attribute can express.