Cookie security: HttpOnly, Secure, SameSite, and the __Host- prefix
A session cookie is a bearer token. Whoever holds the bytes is the user, no questions asked, until the cookie expires or the server revokes it. That is the whole security model, and it is why a single stolen cookie is worth more than a stolen password to anyone who wants into an account right now without tripping a login alert. The attributes on a Set-Cookie line exist to narrow the conditions under which that bearer token leaves the browser, and to stop attackers from forging or reading it. None of them encrypt the cookie. None of them authenticate it. Each one closes off one specific path an attacker would otherwise take.
The trouble is that the attributes accreted over twenty years, each bolted on to patch a class of attack that the original design did not anticipate, and the result is a feature set where the names lie. Secure does not make a cookie secure. HttpOnly has nothing to do with HTTP versus HTTPS. SameSite=Strict is not strict enough on its own to stop CSRF in every case, and SameSite=Lax, the modern default, has a documented window where it does nothing. This post is a reference for what each attribute actually enforces at the browser, what threat it was built for, and where it leaks. It cross-links the cookie identity layer post for the SameSite and partitioning history, and stays in its lane here: the security attributes, one at a time, plus the prefixes that try to make the whole set tamper-evident.
The sections that follow walk the attributes roughly in the order they were added. Domain and Path scoping first, because they define what a cookie even belongs to. Then Secure and HttpOnly, the confidentiality pair from the early 2000s. Then SameSite, the cross-site defense that became the Chrome default in 2020. Then the __Secure- and __Host- name prefixes, which exist because every attribute before them could be quietly overridden by a cookie set somewhere else. The post closes on what the prefixes still cannot guarantee, including a 2025 bypass class that turns a parsing disagreement between the browser and the backend into a forged __Host- cookie.
The shape of a Set-Cookie line
Everything here is one HTTP response header. The server sends Set-Cookie, the browser parses it, stores the cookie, and decides on every subsequent request whether to attach it. The attributes are directives to that storage-and-attach logic. A representative line:
Set-Cookie: __Host-sid=a1b2c3; Path=/; Secure; HttpOnly; SameSite=LaxThe name-value pair is the only part the server actually reads back later. Everything after the first semicolon is an instruction to the browser about how to treat the pair, and the browser never sends those attributes back. When the cookie returns, it returns naked:
Cookie: __Host-sid=a1b2c3That asymmetry is the root of half the problems in this post. The server sets Secure; HttpOnly; SameSite=Lax; Path=/ but on the way back it receives only __Host-sid=a1b2c3, with no way to tell from the Cookie header which attributes the browser enforced, whether the cookie came over HTTPS, or even which of several same-named cookies it is looking at. The attributes govern the browser’s behavior. They are not echoed, not signed, and not verifiable server-side after the fact.
The current normative reference for all of this is the IETF draft known as 6265bis, which has been the editors’ working revision of the cookie spec for years. As of the December 2025 revision (draft 22) the editors are Steven Bingler and Mike West of Google and John Wilander of Apple. The original is RFC 6265, published in 2011 by Adam Barth, which itself codified behavior that browsers had shipped since the Netscape era. A newer effort, the layered-cookies draft, began in late 2025 and is intended to eventually obsolete both; it does not change the attribute semantics described here, it reorganizes the spec text. So when this post quotes “the spec,” it means 6265bis unless noted.
Domain and Path: what a cookie belongs to
Before any security attribute means anything, the browser has to decide which cookie a Set-Cookie line creates and which requests it rides on. Two attributes control that scope, and both predate every security feature in this post.
The Domain attribute widens a cookie’s reach. With no Domain attribute, a cookie is host-only: it goes back only to the exact host that set it. A cookie set by app.example.com with no Domain attribute is sent to app.example.com and nowhere else. Set Domain=example.com and the cookie becomes available to example.com and every subdomain under it, so app.example.com, www.example.com, and www.corp.example.com all receive it. The spec is explicit that there is no way to scope a cookie to a single subdomain and a parent at once, and no way to set a Domain that the setting host is not part of. You can widen toward the registrable domain; you cannot set a cookie for a sibling or for an unrelated site.
The Path attribute narrows by URL prefix. A cookie with Path=/admin is attached only to requests whose path is /admin or a subdirectory of it. The intent reads like an access-control boundary, and developers reach for it as one. It is not. The spec states plainly that “the Path attribute does not provide any integrity protection because the user agent will accept an arbitrary Path attribute in a Set-Cookie header.” A response to /foo/bar can set a cookie with Path=/qux. More to the point, the same-origin policy does not isolate by path: a page at /admin and a page at /public on the same host share a JavaScript execution context, so script on one path can read cookies scoped to another. Path scoping reduces accidental cookie bloat on requests. It is not a security boundary, and treating it as one is a recurring mistake.
The Domain attribute, meanwhile, is the entry point for a whole attack family. The moment a cookie is shared across subdomains, any subdomain that an attacker controls can write cookies that the parent and its siblings will read. That is cookie tossing, and it is the reason the __Host- prefix exists. We come back to it below.
Secure: confidentiality on the wire, no integrity
The Secure attribute tells the browser to attach the cookie only to requests sent over a secure channel, in practice HTTPS. Set it on a session cookie and the cookie never travels in cleartext where a network observer could read it off the wire. That is the entire guarantee, and the name oversells it.
What Secure does not do is protect the cookie’s integrity. RFC 6265 spelled the gap out in its security considerations, and the warning still holds: “an active network attacker can overwrite Secure cookies from an insecure channel,” disrupting their integrity. The mechanism is worth understanding because it is unintuitive. The Secure attribute governs whether the browser sends an existing cookie over a given channel. It historically did much less to govern who is allowed to set or overwrite a Secure cookie. A network attacker who can inject content into any plaintext HTTP response from a related host, even one the application does not use for anything sensitive, could send a Set-Cookie that overwrites the victim’s Secure session cookie, because the browser’s cookie store was keyed by name, domain, and path without distinguishing the channel that wrote each entry. Confidentiality, yes. Integrity, no.
Browsers tightened this over time, restricting the ability of non-secure origins to clobber Secure cookies, and 6265bis now describes a cookie store that tracks a cookie’s secure-only-flag and refuses certain overwrites from insecure contexts. But the original sin remains visible in the design: Secure was a send-time rule retrofitted with set-time protections, not an integrity primitive from the start. This is the single most important thing to internalize about cookie security. The attributes constrain when a cookie is sent; they were never built to constrain who can write it. Every prefix and partitioning scheme since has been an attempt to bolt write-side guarantees onto a read-side mechanism.
HttpOnly: keeping document.cookie out of the cookie
HttpOnly is the oldest of the security attributes and the most precisely named once you read it correctly. It has nothing to do with the HTTP versus HTTPS distinction. It means the cookie is available only to the HTTP layer, not to non-HTTP scripting APIs. With HttpOnly set, document.cookie in JavaScript cannot read the cookie and cannot overwrite it. The cookie still rides on every matching request; it simply becomes invisible to page script.
The attribute came out of Microsoft, shipping in Internet Explorer 6 Service Pack 1 in 2002 as a mitigation for cross-site scripting cookie theft. The threat model is direct. An XSS flaw lets an attacker run JavaScript in the victim’s origin. The classic payoff is to read document.cookie, exfiltrate the session token to an attacker-controlled server, and replay it. HttpOnly removes the session cookie from document.cookie, so the obvious one-line theft stops working. By 2008, when Jeff Atwood wrote up an XSS worm on Stack Overflow that had hinged on exactly this, the attribute was supported by Internet Explorer 7, Firefox 3, and Opera 9.5, and it is universal now.
The limits are equally direct, and the spec and OWASP both state them. HttpOnly does not prevent XSS; it reduces the value of one XSS payoff. An attacker with script execution in your origin can still drive the application as the user: submit forms, read page content, exfiltrate a CSRF token from the DOM, and make authenticated same-origin requests whose responses they read. The session cookie rides invisibly on all of those requests, doing its job, which is the attacker’s job now too. There is even a historical wrinkle: early browsers exposed response headers through XMLHttpRequest, so a script could issue a same-origin request and read the Set-Cookie value out of the response headers, sidestepping HttpOnly entirely. That hole was closed, but it is a clean illustration of the rule. HttpOnly raises the cost of cookie theft. It does not make a compromised origin safe.
SameSite: the cross-site send rule
Domain, Path, Secure, and HttpOnly all govern a cookie within its own site. None of them address the problem that a cookie is sent on requests initiated by other sites. That is the engine of cross-site request forgery: a malicious page at evil.example causes the victim’s browser to make a request to bank.example, and the browser dutifully attaches the victim’s bank.example session cookie because the request goes to bank.example, regardless of who started it. The cookie is a bearer token and the browser is a confused deputy.
SameSite is the attribute that lets a server opt out of that behavior. It takes three values. SameSite=Strict means the cookie is sent only on same-site requests, full stop; a request that originated from any other site never carries it, even a top-level navigation where the user clicked a link from evil.example to bank.example. SameSite=Lax relaxes that one case: the cookie is sent on same-site requests and also on cross-site top-level navigations that use a safe method, in practice a GET that changes the address bar. Clicking a link to your bank carries the cookie; a cross-site POST, an image load, an iframe, or a fetch does not. SameSite=None is the old unrestricted behavior, the cookie is sent on every request including cross-site subresource loads, and the spec now requires that None be paired with Secure or the cookie is ignored entirely.
The decisive change came in Chrome 80, rolled out starting February 2020: a cookie with no SameSite attribute is now treated as Lax by default rather than the historical None. The Chromium ecosystem followed, and the unspecified-equals-Lax default is now standard across Chrome, Edge, and Opera, with Firefox and Safari applying their own cross-site cookie restrictions. The practical effect is that the dangerous default flipped. A developer who sets a session cookie and forgets SameSite gets Lax protection for free, and a developer who genuinely needs a cross-site cookie has to ask for it explicitly with SameSite=None; Secure. That single default change retired a large fraction of latent CSRF exposure on the web without anyone touching their code.
The word “site” is doing heavy lifting and it is not the same as “origin.” Same-site means the same registrable domain, the eTLD+1. app.example.com and intranet.example.com are same-site even though they are different origins. A request from one to the other is a cross-origin request but a same-site one, so SameSite does not restrict it at all. This is the seam that the CORS and same-origin policy post covers from the other direction, and it matters here because it means an XSS or an open redirect on any sibling subdomain can launch requests that SameSite treats as friendly. The defense scopes to the whole registrable domain, so it is only as strong as the weakest host under that domain.
There is a second, sharper limit on Lax specifically, and it is the one that turns up in real exploitation. The documented Chrome behavior is that the default Lax treatment includes a roughly two-minute grace period: for the first 120 seconds after a cookie is set, a top-level cross-site POST will still carry a cookie that lacks an explicit SameSite attribute. This was added so that login flows and OAuth callbacks, which often round-trip through a third party and come back with a POST, would not break under the new default. The cost is a window. If an attacker can cause a fresh cookie to be issued, for instance by kicking off a login or an OAuth handshake, and then immediately drive a cross-site POST, the grace period can let that POST carry the just-issued cookie. The mitigation is not to rely on the implicit default but to set SameSite explicitly and, for anything that changes state, to keep a real CSRF token or origin check in place. Lax is a strong baseline. It is not a complete CSRF control, and the content security policy and CSRF-token layers still earn their keep.
A further subtlety: SameSite=Lax only protects state changes that use unsafe methods. If an application performs a state-changing action on a GET, the cookie rides along on a top-level cross-site navigation by design, and Lax does nothing to stop it. Some servers also honor method-override conventions, where a header or hidden form field turns a GET into a logical POST on the backend; if such an endpoint mutates state and relies on Lax for protection, a top-level GET navigation can drive it with the cookie attached. Keep state changes off GET, and the second of those problems disappears.
The injection problem the attributes cannot solve
Step back and notice what every attribute so far has in common. Secure, HttpOnly, SameSite, Domain, Path: each is a property the legitimate server attaches to a cookie. None of them stops a different writer from setting a cookie of the same name with weaker properties. The cookie store is keyed by name, domain, and path. It is not keyed by who set the cookie or how trustworthy they were.
That gap is cookie tossing. The attack needs one foothold: control of a subdomain, or the ability to set a cookie from a related host. That foothold can come from a subdomain takeover (a dangling DNS record pointing at a deprovisioned cloud service), an XSS on a less-defended subdomain, or a network position on a plaintext HTTP connection to any related host. From there, the attacker sets a cookie with Domain=example.com, the broad scope, so it is delivered to the real application. They give it the same name as the application’s cookie. Now the victim’s browser holds two cookies with that name, and when it sends them both, the application has to pick one. Depending on path and domain specificity, the attacker’s cookie can win, or can simply be the one the application reads first.
The payoffs vary. The cleanest is session fixation: the attacker fixes a known value as the victim’s session cookie before login, the victim authenticates, and because the server bound the session to a cookie value the attacker chose, the attacker now shares the session. Other variants poison a multi-step flow, swap a CSRF token, or hijack an OAuth state parameter carried in a cookie. The 2025 write-ups of cookie tossing against OAuth flows and webmail show the pattern is alive and chained into real exploits, often paired with a self-XSS that would otherwise be unexploitable. The SSRF and cloud metadata and web cache deception posts cover other cases where the boundary an application assumed turns out to be porous; this is the cookie-layer instance of the same lesson. A name is not an identity. The store has no idea who wrote what.
The prefixes: making the cookie name carry a guarantee
The fix that shipped is clever in its minimalism. Rather than add yet another attribute that an attacker’s cookie could simply omit, the proposal, authored by Mike West in 2015 and first implemented in Chrome 49 and Firefox 50 in 2016, put the guarantee in the cookie’s name. Two reserved prefixes, and a browser rule: if a cookie’s name starts with one of these prefixes, the browser refuses to store it unless it meets the prefix’s requirements. Because the name travels with the cookie and the application reads the name, an attacker cannot strip the prefix to dodge the rule without also changing which cookie they are forging.
__Secure- is the weaker of the two. A cookie whose name begins with __Secure- is accepted only if it was set with the Secure attribute over a secure connection. It may still carry a Domain attribute, so it can be shared across subdomains. That single constraint, that the cookie must be Secure, closes the network-injection overwrite from a plaintext channel, because a non-secure context cannot create a conforming __Secure- cookie at all.
__Host- is the one that matters for cookie tossing. A __Host- cookie is accepted only if it was set with Secure, over HTTPS, with Path=/, and with no Domain attribute. The missing Domain is the whole point. A cookie with no Domain is host-only: it locks to the exact host that set it and is never sent to or settable from a subdomain or a parent. A malicious subdomain cannot set a __Host- cookie that the parent will read, because any __Host- cookie it tries to set is bound to the subdomain itself. The prefix turns the host-only property from an opt-in default that any cookie can override into a name-enforced invariant. The matching is case-insensitive, so __HOST- and __host- trigger the same enforcement. The spec text in 6265bis is precise: a __Host- cookie “will have been set with a Secure attribute, a Path attribute with a value of /, and no Domain attribute,” and the lack of a Domain attribute “ensures that the cookie’s host-only-flag is true, locking the cookie to a particular host.”
In practice the advice that falls out of this is short. Name session and CSRF cookies with the __Host- prefix unless you have a concrete reason to share them across subdomains, in which case __Secure- with an explicit Domain is the next best thing. The prefixes cost nothing, they are widely supported, and they convert a class of silent override into a hard browser rejection. The partitioned-cookie work in CHIPS leans on the same idea and recommends pairing Partitioned with __Host- so that a cross-site cookie is both partitioned and host-locked.
Where the prefixes still leak
The prefix guarantee lives entirely at the boundary where the browser parses a Set-Cookie header and the backend later parses a Cookie header. If those two parsers disagree about where a cookie name begins, the guarantee can be sidestepped, and that is the substance of the bypass class PortSwigger’s Zakhar Fedotkin published in September 2025 under the name Cookie Chaos.
The mechanism is a parsing disagreement, not a flaw in the prefix rule itself. RFC 6265 treats the cookie header as a sequence of octets rather than characters, so a browser checks the prefix against the raw bytes it stored. A backend framework, meanwhile, often normalizes the cookie before it reads the name: stripping whitespace, decoding, or switching parsing modes on a legacy marker. When the normalization the backend applies is one the browser did not, a cookie that the browser stored as something other than a __Host- cookie can be read by the backend as a __Host- cookie. The reported variants include leading Unicode whitespace that some server-side strip() implementations remove but the browser keeps as part of the name, and a legacy parsing mode in some Java servers triggered by an old cookie-version marker. Different stacks were affected to different degrees; the write-up notes a framework whose whitespace normalization opened the door, and a browser that resisted multibyte cases but not every single-byte one.
The defensive reading is the important part, and it generalizes past this one bug. The prefix is a browser-enforced invariant on the browser’s view of the cookie. It is not a signature, and it does not survive a second parser that sees the bytes differently. So the prefix is necessary but not sufficient. A backend that trusts a __Host- name as proof the cookie is host-locked, while running it through a normalizer that disagrees with the browser about the name’s boundaries, has reintroduced the very injection the prefix was meant to block. The mitigation is server-side: validate the cookie’s contents independently, bind sessions to server-side state rather than to a cookie value the client can fix, and do not let a subdomain’s word be load-bearing. None of that is new advice. The bypass is a reminder that an attribute or prefix enforced in one place is not enforced everywhere the value travels.
This is also where the broader architecture reasserts itself. Cookies have no integrity primitive of their own. A cookie that must not be forged should carry a value the server can verify (a signed or random server-side session identifier, not a client-meaningful payload), so that even a successfully injected cookie is just an invalid token. The attributes and prefixes reduce the opportunities for injection and theft. They do not replace authenticating the cookie’s value on the server.
What is settled and what is moving
The attribute set is stable in a way most of the web platform is not. HttpOnly and Secure have meant the same thing for two decades. The prefixes have meant the same thing since 2016. The one genuinely recent shift is the 2020 default flip to SameSite=Lax, which changed the behavior of every cookie that does not specify the attribute, and that has now bedded in across the major engines. The cookie spec itself is mid-reorganization, with the layered-cookies draft begun in late 2025 set to eventually replace 6265bis, but that work is editorial structure rather than new attribute semantics.
The thing that did not happen is worth recording because so much was planned around it. Google spent years signaling the end of third-party cookies in Chrome, then in April 2025 Anthony Chavez announced that Chrome would not ship the user-choice prompt and would not unilaterally deprecate third-party cookies, leaving today’s controls in place. Safari has blocked third-party cookies by default since 2020 and Firefox isolates them per top-level site with Total Cookie Protection, so the cross-site cookie is already dead or partitioned for most non-Chrome users regardless of what Chrome decided. The security attributes in this post are orthogonal to that fight. They govern a cookie’s behavior on the site that set it, and they keep doing their job whether or not third-party cookies survive.
If there is one sentence to carry out of all this, it is the one the design keeps proving: cookie attributes constrain when a cookie is sent and who can read it, never what it is worth. A __Host- Secure HttpOnly SameSite=Strict session cookie is still a bearer token, and the server that issued it still has to treat the value as untrusted until it has matched it against state it controls. The attributes shrink the attack surface to a few well-understood seams: an XSS that acts as the user without reading the cookie, a same-site sibling that SameSite waves through, a backend parser that disagrees with the browser about where a name begins. Close those, and the cookie does what it was always supposed to do, which is identify a session and nothing more.
Sources & further reading
- Bingler, West, and Wilander (2025), Cookies: HTTP State Management Mechanism (draft-ietf-httpbis-rfc6265bis-22) — the current normative cookie spec, including the
__Secure-/__Host-prefix rules and SameSite semantics. - Barth (2011), RFC 6265: HTTP State Management Mechanism — the 2011 cookie RFC; its security considerations spell out the Secure-overwrite and Path-integrity weaknesses.
- West (2015), Cookie Prefixes (draft-west-cookie-prefixes-00) — the original proposal that put the security guarantee into the cookie name.
- Atwood (2008), Protecting Your Cookies: HttpOnly — the Stack Overflow XSS-worm story and the history of HttpOnly’s introduction in IE 6 SP1.
- OWASP, HttpOnly — what the attribute does and the precise limits of its XSS mitigation.
- Google Search Central (2020), Get Ready for New SameSite=None; Secure Cookie Settings — the Chrome 80 default-to-Lax rollout and the None-requires-Secure rule.
- web.dev (2019, updated), SameSite cookies explained — the canonical developer reference for Strict, Lax, and None and the site-versus-origin distinction.
- PortSwigger Web Security Academy, Bypassing SameSite cookie restrictions — the Lax two-minute window, same-site sibling bypasses, and method-override pitfalls.
- Fedotkin (2025), Cookie Chaos: How to bypass __Host and __Secure cookie prefixes — the browser-versus-backend parsing-disagreement class that can forge prefixed cookies.
- Calzavara et al. (2020), Can I Take Your Subdomain? Exploring Related-Domain Attacks in the Modern Web — a measurement study of subdomain takeover and cookie-tossing exposure across real sites.
- MDN Web Docs, Cookies Having Independent Partitioned State (CHIPS) — the Partitioned attribute and its recommended pairing with
__Host-. - Chavez / Privacy Sandbox (2025), Next steps for Privacy Sandbox and tracking protections in Chrome — the April 2025 decision not to deprecate third-party cookies in Chrome.
Further reading
The cookie and identity layer: SameSite, partitioning, and the third-party-cookie death
Traces the HTTP cookie from a 1994 shopping-cart hack to the web's identity layer: how SameSite reshaped it, why the third-party-cookie phase-out collapsed in 2024-2025, and what partitioning leaves behind.
·18 min readCHIPS and partitioned cookies: the post-third-party-cookie identity model
A primary-source walk through CHIPS: the Partitioned cookie attribute, the double-keyed cookie jar, the cross-site ancestor chain bit, the 10 KiB per-partition budget, and where it sits now that Privacy Sandbox is gone.
·21 min readCORS, the same-origin policy, and the long history of cross-origin trust
Traces the same-origin policy from Netscape 1995 to RFC 6454, then how CORS relaxes it through preflights and Access-Control headers, the misconfigurations that break it, and where the model stands in 2026.
·21 min read