Skip to content
KEDBYTE
How Identity Works
Chapter
37

The Cookie

Part IV · Identity Between Systems|11,486 words|about 50 min read|Volume 4

37.0 What this chapter gives you#

  1. You will be able to read any Set-Cookie header aloud and say what every attribute on it does and which attack it was added to stop.
  2. You will be able to explain why Domain=northbank.example makes a cookie reach further rather than less far, and why that catches people out.
  3. You will be able to state precisely what Secure blocks, precisely what HttpOnly blocks, and the large set of things that neither of them blocks.
  4. You will be able to name, for each of the three SameSite values, the exact request types that still carry the cookie and the ones that do not.
  5. You will be able to describe what Chrome changed in February 2020, what broke, and why the repair for the breakage created a new opening.
  6. You will be able to choose correctly between the __Secure-, __Host-, __Http- and __Host-Http- name prefixes and say what guarantee each one buys.
  7. You will be able to explain partitioned cookies and give the position of third-party cookies in each major browser as of August 2026.
  8. You will be able to walk through a cross-site request forgery attack and show, step by step, why SameSite on its own is not a complete defence.
  9. You will be able to write a hardened Set-Cookie header for a login session and defend every character of it in a code review.

A cookie is the smallest piece of identity machinery on the web and the one that carries the most weight. It is a short note that a website asks your browser to keep and to hand back on later visits. Almost every logged-in web session in the world rests on one. The note itself is usually meaningless, a random string, and it is the fact that your browser hands it back at the right moment, and refuses to hand it back at the wrong one, that turns it into proof of who you are.

The refusals are the interesting part. A cookie by itself has no security properties at all. Everything that makes it safe is written in the attributes that travel beside it: Secure, HttpOnly, Domain, Path, SameSite, Partitioned, and the small vocabulary of magic name prefixes. Each of those was added to the web after somebody was robbed. Not one of them was designed in advance. The list of cookie attributes is a list of attacks, written backwards, and the order in which they appear in the specification is roughly the order in which the attacks became too expensive to ignore.

That history has a consequence that this chapter is built around. Because the attributes were added one at a time, each one closes a different hole, and they do not cover for each other. A cookie can be Secure and HttpOnly and still be stolen. It can be SameSite=Strict and still be forged around. It can carry every attribute in the specification and still be handed to an attacker by a subdomain you forgot you owned. Getting eight attributes right and one wrong does not give you eight-ninths of the protection. Very often it gives you none of it, because the one you got wrong is the one the attacker walks through.

The chapter before this one dealt with the session itself: what a session identifier is, how long it must be, and how a server decides that a session is over. That is chapter 36, and this chapter does not repeat it. The chapter after this one deals with bearer tokens, where holding the credential is the whole of the proof, and that is chapter 38. This chapter sits between them and covers the container: the exact rules by which a browser stores a small named value, decides which requests it belongs on, and refuses to give it to script. We will use one worked example throughout, a bank called northbank.example, and we will carry the same cookie from the plain explanation all the way to a hardened header at the end.

The plain version#

The cloakroom ticket#

Imagine a theatre with a cloakroom. You arrive, hand over your coat, and the attendant gives you a small numbered ticket. The ticket says 41. It does not say your name. It does not describe your coat. It is a number, and the only reason it works is that the attendant wrote the same number on the hanger.

At the end of the evening you hand the ticket back and get your coat. The whole system rests on two facts. The first is that the attendant remembers which hanger 41 refers to. The second is that nobody else has a ticket that says 41.

A website works the same way. You type your username and password once. If they are right, the site puts a hanger in its own back room with your details on it, writes a number on a small note, and gives the note to your browser, which is the program you use to look at websites. From then on, your browser sends that note along with every request it makes to that site. A request is simply your browser asking the site for a page or for an action: show me my balance, transfer this money, log me out.

The note is a cookie. That is the whole idea. The site does not recognize you by your face or your typing or your machine. It recognizes the note.

There is one difference from the theatre, and it matters a great deal later. In a theatre you hand your ticket over deliberately, when you choose. On the web your browser hands the note over automatically, without asking you, every single time it talks to that site. You never decide. The browser decides, using rules that were written down for it in advance.

What is written on the note, and what is not#

The note has two parts that the site cares about: a name and a value. The name is something like sid, short for session identifier. The value is a long random string of letters and digits, and it means nothing at all on its own. It is the cloakroom number, chosen to be so hard to guess that no stranger will ever write down the same one.

Along with the note, the site sends a short list of instructions about when the browser may hand it back. Those instructions are not written on the note itself. They are told to the browser once, at the moment the note is given out, and the browser remembers them privately. This is the single most important sentence in the plain version: when the browser hands the note back later, it hands back only the name and the value. The instructions do not travel back. The site sees sid=9f2c7a1e... and nothing else.

Think of it as the attendant whispering the rules to you as she gives you the ticket. “Only bring this back to this desk. Only through the guarded corridor. Never let anyone in the lobby copy it. And if a stranger sends you here on an errand, do not use it.” You remember the rules. The ticket itself is blank on the back. When you present it, the attendant cannot tell from looking at it which rules you were given.

Every rule is a scar#

There are six or seven rules a site can attach to a note, and here is the thing worth carrying away from this chapter: not one of them was there at the start. Each was added after a specific theft.

The first rule is about which desks accept the ticket. A theatre might have a main cloakroom and a smaller one upstairs. If the ticket is marked for the main desk only, taking it upstairs achieves nothing. On the web the equivalent question is which addresses the note gets sent to. By default a note goes back only to the exact address that issued it. A site can widen that, so the note also goes to related addresses, and widening it is the only direction the setting moves. There is no way to use this rule to narrow the note down to less than the address that issued it. That asymmetry has cost people a lot of money.

The second rule is about the corridor. Early websites sent everything in the clear, so anyone sitting between you and the theatre could read the ticket number as it went past and walk in with a copy. The fix was a rule that says: only hand this back over a protected connection, never over an open one. If the connection is not protected, the browser pretends the note does not exist.

The third rule is about the lobby. Websites run small programs inside your browser, and those programs can normally read the notes the browser is holding. If an attacker manages to smuggle their own program onto a page, which happens more often than anyone would like, that program reads the note and sends it away. The fix was a rule that says: this note is invisible to programs. Only the browser’s own plumbing may touch it.

The fourth rule is about errands. This one is subtle and it is the one people get wrong. Suppose you are logged into your bank in one tab. In another tab you open a page belonging to a stranger. That stranger’s page can quietly tell your browser: go and ask the bank to transfer money. Your browser, being obedient, goes to the bank, and because the rules of the note say nothing about errands, it takes the note with it. The bank sees a properly signed request from a logged-in customer and does as it is told. Nobody stole the note. The attacker never saw it. They simply borrowed it in place. The fix was a rule that says: when a request is started by somebody else’s page, leave this note behind.

The fifth rule is about time. A note can be marked “throw this away when the visitor closes the browser”, or it can be marked with a date. Without any rule about time, notes accumulated for years.

The sixth rule is newer and it is about strangers embedded in the page. Much of what you see on a website is not from that website. Maps, chat boxes, payment forms and adverts are pulled in from elsewhere and shown inside a frame. Those embedded strangers can hold notes of their own, and for twenty years the same note followed them from site to site, which is precisely how you get followed around the internet. The newest rule says: give this embedded stranger a separate note for each site it appears on, so that the note cannot be used to join up your visits.

A worked example: one evening at Northbank#

Let us make it concrete and keep the same example for the rest of the chapter.

You bank with Northbank. You open your browser and go to the bank’s site. You type your customer number and your password. The bank checks them, decides you are you, and writes a hanger in its back room: session 9f2c7a1e4b6d80f35c9e2a7b41d8e0c6 belongs to customer 88214, created at 21:04, good until 21:34 unless you keep using it.

The bank then hands your browser a note with the name sid and that long value, together with its rules: only the bank’s own front address, only over the protected connection, invisible to programs, and never on an errand started by somebody else’s page.

You click “Accounts”. Your browser sends the request, and because the address matches and the connection is protected and the request was started by the bank’s own page, it attaches the note. The bank looks up 9f2c7a1e... on its shelf, finds customer 88214, and shows your balance.

You open a second tab and read a news article. The article page contains a hidden instruction telling your browser to send a transfer of 25,000 rupees to account 447192 at Northbank. Your browser starts the request, then checks the rules on the note. The request was started by the news site, not by the bank, so the errand rule applies, and the note stays behind. Northbank receives a transfer request with no note attached, sees an anonymous stranger asking to move somebody’s money, and refuses.

That refusal is the entire value of a cookie attribute. The money stayed where it was because of one word in a header you will never see.

Where the plain version stops being true#

The cloakroom has several desks, and they share a back room#

The plain version said the note goes back to “the address that issued it”. The truth is messier, and the mess is the source of a large fraction of real cookie bugs.

A web address has three parts that matter for security: the scheme, which is http or https; the host, such as www.northbank.example; and the port, usually 443 and usually invisible. Together these three form what the web calls an origin (the exact combination of scheme, host and port). Almost every security rule in a modern browser is drawn around the origin. Two pages from different origins cannot read each other’s contents.

Cookies do not obey that boundary. They were invented in 1994, before origins were a formal idea, and they are scoped by host and path instead. The port has never been part of a cookie’s identity at all, so a server on port 443 and a completely different application on port 8080 of the same host share one cookie jar, even though the browser treats them as separate origins for everything else. The scheme was in the same position for most of the web’s life: unless a cookie was marked Secure, a plain http page on the host could read and overwrite the cookies of the https site. That is not a bug in one browser. It is what the cookie model has always said.

The honest version: a cookie is not scoped to an origin. It is scoped to a host and a path, and until very recently it ignored the scheme and it still, in most browsers, ignores the port. Two servers on the same host name but different ports share a cookie jar, even though the browser treats them as separate origins for everything else. Chrome has begun binding cookies to the origin that set them, which is covered in the technical half.

The whisper goes one way#

The plain version said the browser remembers the rules privately and does not send them back. That is exactly true, and its consequences are larger than they look.

When your server receives a request, the cookie arrives as bare text: Cookie: sid=9f2c7a1e4b6d80f35c9e2a7b41d8e0c6. There is no field telling you that this cookie was Secure. There is no field telling you it was HttpOnly, or which host set it, or whether it was set by your login endpoint at all or by a script running on a marketing subdomain. Your server cannot tell the difference between the cookie it issued and an identically named cookie planted by someone else.

The honest version: the server has no way to verify how any cookie it receives was created. This is not a gap that better server code can close. It is a property of the protocol. The only repair the web has found is to encode the guarantee in the cookie’s own name, which is what the __Secure-, __Host-, __Http- and __Host-Http- prefixes do, and the reason those odd names exist at all.

The rules are enforced by the coat check, not by the theatre#

Every cookie attribute is an instruction to the browser. The site cannot enforce any of them. If the program at the other end is not a browser, or is an old browser, or is a browser deliberately modified, the attributes mean nothing.

A command-line tool fetching your site will send whatever cookies it likes, in whatever combination it likes, because it is not obliged to implement any of these rules. Malware running on the customer’s own machine can read the browser’s cookie database off the disk. An HttpOnly cookie is invisible to page scripts but perfectly visible to a program with access to the profile directory, which is exactly how the modern cookie-theft industry works.

The honest version: cookie attributes are a contract with a cooperating browser on an uncompromised device. They are excellent against remote attackers who can only speak to the browser through the web. They are worth nothing against an attacker who already owns the machine.

The note is worth exactly as much as the coat#

A cookie carries no proof of who is holding it. Whoever presents it gets the session. That property, called bearer semantics, is the subject of chapter 38, and this chapter will not repeat it. Note only that every attribute we discuss is an attempt to reduce the number of parties who can ever come to hold the cookie, because once someone holds it there is no further check.

Time is a request, not a promise#

The plain version said a site can mark a note with a date. Sites do, and browsers largely honour it, but not exactly.

A browser may throw a cookie away early. It does this when its storage limits are reached, when the user clears their data, when privacy features expire tracking storage, or when the site has not been visited for a long time. It may also refuse to keep a cookie as long as you asked. Since Chrome 104 in August 2022, Chrome silently caps any cookie’s lifetime at 400 days, no matter what date the site sends. Safari caps cookies written by page scripts to seven days.

Equally, a cookie may outlive its stated expiry on the server side. Deleting the session on the shelf is what actually ends the session. Expiring the cookie in the browser only means the browser stops offering it; a copy taken earlier still works until the server forgets the session. The correct handling of that is chapter 36’s material.

The honest version: the expiry attribute is advisory housekeeping for the browser, not a security control. Security expiry belongs on the server.

The technical version#

The header, the grammar, and where the specification actually stands#

The mechanism is older than almost anything else on the web that is still in daily use. Lou Montulli, working at Netscape, had the idea of applying the existing computing term “magic cookie” to web communications in June 1994, and version 0.9beta of Mosaic Netscape, released on 13 October 1994, supported them. Montulli applied for a patent in 1995 and it was granted in 1998 as United States patent 5,774,670. The public found out about cookies only when the Financial Times published an article about them on 12 February 1996, more than a year after they had been quietly deployed in every copy of the most popular browser in the world. Formal specification followed: RFC 2109 in February 1997 by David Kristol and Lou Montulli, RFC 2965 in October 2000, which introduced a parallel Set-Cookie2 header field that browsers never adopted, and RFC 6265 in April 2011, which threw away the parts nobody had implemented and wrote down what browsers actually did.

A server creates a cookie by sending a Set-Cookie header field in a response. One header field per cookie. The syntax in the current draft is:

set-cookie        = set-cookie-string
set-cookie-string = BWS cookie-pair *( BWS ";" OWS cookie-av )
cookie-pair       = cookie-name BWS "=" BWS cookie-value
cookie-name       = token
cookie-value      = *cookie-octet /
                    ( DQUOTE *cookie-octet DQUOTE )
cookie-octet      = %x21 / %x23-2B / %x2D-3A /
                    %x3C-5B / %x5D-7E
cookie-av         = expires-av / max-age-av / domain-av /
                    path-av / secure-av / httponly-av /
                    samesite-av / extension-av

Read the last production carefully. cookie-av is the complete list of attributes the specification defines: Expires, Max-Age, Domain, Path, Secure, HttpOnly, SameSite, and anything else, which is swept into extension-av and ignored by browsers that do not recognize it. Partitioned, which we come to later, is an extension in exactly this sense as of August 2026.

Now read the grammar for the request direction, which is where the asymmetry lives:

cookie        = cookie-string
cookie-string = cookie-pair *( ";" SP cookie-pair )

That is all. Names and values, separated by semicolons. No attributes, no ordering guarantee that servers may rely on, no indication of which host set which cookie. A worked exchange for Northbank looks like this:

POST /login HTTP/1.1
Host: www.northbank.example

HTTP/1.1 302 Found
Location: /accounts
Set-Cookie: __Host-sid=9f2c7a1e4b6d80f35c9e2a7b41d8e0c6;
 Secure; HttpOnly; Path=/; SameSite=Lax

GET /accounts HTTP/1.1
Host: www.northbank.example
Cookie: __Host-sid=9f2c7a1e4b6d80f35c9e2a7b41d8e0c6

The header value is shown wrapped across two lines here for the printed page; on the wire it is a single line.

The specification position as of August 2026 is worth stating exactly, because it is unusual. The published standard is RFC 6265, “HTTP State Management Mechanism”, by Adam Barth, April 2011. It does not contain SameSite. It does not contain cookie name prefixes. It does not contain the 400-day cap, the public suffix rules in their current form, or the same-site algorithm. Every one of those lives in the revision, draft-ietf-httpbis-rfc6265bis, whose current version is draft 22, dated 1 December 2025, by Steven Bingler, Mike West and John Wilander. That draft has passed IESG evaluation and sits in the RFC Editor queue, awaiting publication as a Standards Track RFC that will obsolete RFC 6265.

There is a further layer. In parallel the HTTP working group has adopted draft-ietf-httpbis-layered-cookies, by Anne van Kesteren of Apple and Johann Hofmann of Google, whose version 02 is dated 21 May 2026. It restates the same mechanism in the layered style used by the Fetch and URL specifications, and is intended in time to obsolete both RFC 6265 and 6265bis.

So: the behaviour every browser has implemented since 2020 is specified in a draft that has not yet been published as an RFC, while the published RFC describes a web that no longer exists. If you are ever asked to cite “the cookie RFC” for SameSite, the honest answer is that RFC 6265 does not define it and you must cite 6265bis by draft number and date.

The specification’s size limits matter in practice. RFC 6265 section 6.1 asks user agents to provide at least 4096 bytes per cookie, measured as the sum of the cookie’s name, value and attributes; at least 50 cookies per domain; and at least 3000 cookies in total. The 6265bis draft keeps the same shape and states the rejection rule directly: if the sum of the lengths of the name string and the value string is more than 4096 octets, the browser ignores the whole Set-Cookie string. Separately, 6265bis warns that although the HTTP specification sets no limit on header field length, “many popular implementations have default limits of 8192 octets”, so a site that sets forty cookies can produce a Cookie header that its own web server rejects with a 400 or 431 before the application ever sees it.

Limit Value Where it comes from
Cookie name plus value 4096 octets RFC 6265bis
Cookies per domain 50 minimum RFC 6265 6.1
Cookies in total 3000 minimum RFC 6265 6.1
Typical header limit 8192 octets Server defaults

Domain: the attribute that only ever widens#

When a server sends no Domain attribute, the browser stores the cookie with its host-only flag set to true, and sends it back only to the exact host that set it. A cookie set with no Domain by www.northbank.example is never sent to promo.northbank.example and never sent to northbank.example.

When a server does send Domain, the host-only flag is false and the cookie is sent to that domain and to every host beneath it. The rule the browser applies is called domain-matching: the request host matches the cookie’s domain if the two strings are identical, or if the cookie’s domain is a suffix of the request host, the character immediately before the suffix in the request host is a dot, and the request host is a name rather than a bare IP address.

The consequence is the sentence in this chapter’s title claim. Domain widens. It never narrows.

Set-Cookie: sid=...                    (no Domain)
  goes to: www.northbank.example

Set-Cookie: sid=...; Domain=northbank.example
  goes to: northbank.example
           www.northbank.example
           promo.northbank.example
           anything.deeper.northbank.example

Two further rules constrain it. First, a host may only set a cookie for a domain that domain-matches itself, so evil.example cannot set a cookie for northbank.example. Second, a browser refuses a Domain value that is a public suffix, using the Public Suffix List maintained by Mozilla, so nobody can set a cookie for .com, .co.uk or .in and have it sent to every site under it. Both of those rules were added in response to attacks; the second closed a class of attack known as cross-site cooking, in which a site sets a cookie for a whole top-level domain and has it delivered to every site underneath. A leading dot, as in Domain=.northbank.example, is stripped and has no effect; it is a relic of RFC 2109 from February 1997, where the dot was meaningful.

The security problem Domain creates is the one people miss. Widening the cookie to northbank.example means every subdomain now receives your session cookie on every request. If promo.northbank.example is a marketing site run by an agency on a shared hosting platform, that agency now receives your customers’ live session cookies in their access logs. Worse, the relationship runs the other way as well: any subdomain can set a cookie for the parent domain, so a compromised promo.northbank.example can set sid for northbank.example and your main site will receive it. The server cannot tell which one it is looking at, because the Cookie header carries no origin information.

That second direction is a real and studied attack. The 2015 USENIX Security paper “Cookies Lack Integrity: Real-World Implications”, by Xiaofeng Zheng, Jian Jiang, Jinjin Liang, Haixin Duan, Shuo Chen, Tao Wan and Nicholas Weaver, demonstrated it against production sites including major banks and Google’s own properties, using cookie injection from related hosts and from network positions. The defence is not to stop using subdomains. The defence is to make the cookie host-only and prove it in the name, which is what __Host- does.

Path: a filing rule, not a fence#

Path restricts a cookie to a subtree of URLs. If it is absent, the browser computes a default path from the request URI by a fixed algorithm: take the path portion; if it is empty or does not begin with a slash, use /; if it contains no more than one slash, use /; otherwise use everything up to but not including the rightmost slash. A cookie set by a response to /accounts/statements/2026 with no Path therefore gets Path=/accounts/statements.

Sending it back uses path-matching: the request path matches the cookie path if they are identical; or if the cookie path is a prefix of the request path and the cookie path ends in a slash; or if the cookie path is a prefix and the first character of the request path not covered by the prefix is a slash.

The important professional fact is that Path provides no security isolation whatsoever. The 6265bis draft says so plainly, noting that although the attribute seems useful for isolating cookies between paths on a host, it cannot be relied upon for security. The reason is the origin model described earlier: two pages on the same host are same-origin regardless of path, so a page at /promo/index.html can create a hidden frame pointing at /accounts/, reach into that frame through the browser’s scripting interface because it is the same origin, and read the cookies scoped to /accounts/ from inside it. Path keeps cookies out of the wrong request. It does not keep them away from the wrong script.

Use Path=/ for session cookies. It is the only value compatible with the __Host- prefix, and any narrower value produces the illusion of a boundary that does not exist.

Expires, Max-Age, and the 400-day ceiling#

Expires takes an HTTP date. Max-Age takes a number of seconds and wins where both are present. A cookie with neither is a session cookie and is discarded when the browsing session ends, though “ends” is looser than it sounds: browsers that restore tabs on restart also restore session cookies.

Deleting a cookie is done by setting it again with an expiry in the past, and the deletion only works if the Domain and Path match the original exactly. This is the single most common cookie bug in production logout code. A cookie set with Domain=northbank.example cannot be deleted by a header that omits Domain; the browser simply creates a second, host-only cookie of the same name, and now sends both, in an order the specification does not usefully constrain.

Set-Cookie: __Host-sid=; Max-Age=0; Secure; HttpOnly;
 Path=/; SameSite=Lax

Since Chrome 104, released in August 2022, Chrome caps cookie lifetime at 400 days, chosen so that a site visited once a year still works. The 6265bis draft has adopted the same figure as a recommendation, saying the limit should not be greater than 400 days, expressed as 34,560,000 seconds. Safari’s Intelligent Tracking Prevention applies a much harder rule to cookies written by page script rather than by a header, capping them at seven days, a restriction extended to all script-writable storage in the WebKit release of 24 March 2020.

The HTTP Archive’s Web Almanac cookies chapter, based on a crawl of the top million sites in July 2025, found the median cookie expiry across the web to be a full year and the median cookie size to be 40 bytes.

Secure: three separate guarantees, not one#

Secure is usually described as “only sent over HTTPS”. That is one of three distinct things it does in the current draft, and the other two are the ones that matter against a network attacker.

First, sending. If a cookie’s secure-only flag is true and the request is not over a connection the browser considers secure, the cookie is not attached. This is the familiar half.

Second, setting. The storage model in 6265bis step 13 says that if the request URI does not denote a secure connection and the cookie’s secure-only flag is true, the browser aborts and ignores the cookie entirely. A plain http page cannot create a Secure cookie.

Third, and most importantly, overwriting. Step 16 of the same algorithm says that a newly created cookie arriving over a non-secure connection is ignored if the cookie store already contains a matching cookie whose secure-only flag is true. This rule arrived through Mike West’s draft memorably titled “Leave Secure Cookies Alone” and closed a genuine hole: before it, an attacker who could inject content into any plain-HTTP page on any host that domain-matched your site could overwrite your Secure session cookie with one of their own choosing, without ever needing to break TLS.

What Secure does not do is equally precise. It does not encrypt anything; TLS does that. It does not stop page script from reading the cookie. It does not prevent the cookie from being sent cross-site. And it does not, on its own, tell your server anything, because the attribute never comes back.

HttpOnly: what it takes away from script#

HttpOnly limits the cookie to HTTP requests. Concretely, the browser omits the cookie from non-HTTP APIs: it is not visible in document.cookie, and it is not returned by the Cookie Store API. The storage model also blocks the other direction, in step 15: a cookie received from a non-HTTP API with the http-only flag set is ignored, and a script cannot modify or delete an existing HttpOnly cookie.

Microsoft introduced the attribute in Internet Explorer 6 Service Pack 1 in 2002, specifically to reduce the damage from cross-site scripting. It works. An injected script on a page can no longer read the session identifier and post it to a collection server.

Be exact about what remains. OWASP’s own community page states it plainly: HttpOnly does not block cross-site scripting and does not stop script execution, so other consequences such as page defacement or theft of anti-forgery tokens are still possible. More sharply, an attacker with script execution on your origin does not need to read your cookie at all. They can simply make requests from inside the page, and the browser will attach the cookie for them, because the request is same-origin. HttpOnly converts cookie theft into cookie use. That is a genuine improvement, because a stolen cookie works from anywhere for as long as the session lives, while script-driven abuse only works while the victim has the page open, but it is not a fix.

The Web Almanac’s July 2025 crawl found that only about 12% of first-party cookies and a little over 26% of third-party cookies carry HttpOnly.

SameSite: the three modes and exactly what each permits#

SameSite is the attribute that addresses errands. It was introduced by Mike West and Mark Goodwin and first shipped in Chrome 51 in 2016.

Its unit is not the origin but the site, meaning the registrable domain: the domain one label below the relevant public suffix. www.northbank.example and promo.northbank.example are the same site. northbank.example and evil.example are not. Note carefully that same-site is a much weaker statement than same-origin, and the entire difference is where subdomain attacks live. The current draft also makes the comparison “schemeful”, so the scheme now participates in the site comparison:

same site      www.northbank.example
               promo.northbank.example

not same site  northbank.example
               evil.example

not same site  the http and https forms of the same host,
               once the comparison is schemeful

The three values, exactly as the draft defines them:

Strict. The cookie is attached only to same-site requests. It is not sent on any cross-site request, including a top-level navigation triggered from a cross-site document. If a customer clicks a link to your bank from an email, a search result or any other site, the browser sends the first request without the cookie, so the bank sees a logged-out visitor even though the session is alive.

Lax. The cookie is attached to same-site requests, and additionally to cross-site top-level navigations that use a safe method. “Safe” is defined in RFC 9110 as GET, HEAD, OPTIONS and TRACE. Top-level means the whole page navigates; a request that loads a frame, an image, a stylesheet or a background fetch is not a navigation and does not qualify. So a link click carries the cookie, a cross-site form POST does not, and a cross-site image or iframe does not.

None. The cookie is attached to same-site and cross-site requests alike, which is the pre-2020 behaviour. The draft makes this conditional: if the cookie’s same-site flag is None, the browser aborts and ignores the cookie entirely unless the secure-only flag is true. SameSite=None without Secure is not a weak cookie; it is a discarded cookie.

Request type Strict Lax None
Same-site anything sent sent sent
Cross-site link click no sent sent
Cross-site form POST no no sent
Cross-site image, frame no no sent

There is a fourth, unnamed mode: a cookie whose SameSite value is missing or unrecognized. The draft says such a value falls back to a default enforcement mode equivalent to Lax. In Chrome this default is not quite Lax. Chrome applies a more permissive variant, usually written “Lax plus POST”, under which a cookie with no SameSite attribute is still sent on a cross-site top-level POST if it was set less than two minutes before the request. That two-minute window is an implementation detail, not a specification rule, introduced as a temporary compatibility measure in 2020 and still documented as browser behaviour as of August 2026. Any cookie relying on it is relying on a window an attacker can arrange to be inside.

February 2020: the default flip and the breakage#

Before 2020, a cookie with no SameSite attribute was sent everywhere, which meant the web’s default was the least safe option available. Chrome decided to change the default. The rollout is worth knowing in detail, because it is the clearest example in web history of a security default meeting the installed base.

Date What happened
4 Feb 2020 Chrome 80 stable, flag off
17 Feb 2020 Enforcement for small group
3 Apr 2020 Rolled back for COVID-19
14 Jul 2020 Resumed with Chrome 84
11 Aug 2020 Target raised to 100%

Chrome 80 reached stable on 4 February 2020 with the new behaviour present but not enabled. Limited enforcement began on 17 February and expanded through early March. On 3 April 2020 Chrome rolled the change back entirely, stating that in light of the extraordinary global circumstances due to COVID-19 they were temporarily removing the enforcement, so that banking, grocery, government and healthcare sites would not break while people depended on them. On 28 May Chrome announced a July restart; enforcement resumed on 14 July 2020 with Chrome 84 and applied to Chrome 80 and later; the target population was raised to 100% on 11 August 2020. The flags were removed in March 2021 with Chrome 91, at which point the behaviour was simply how Chrome worked.

What broke, in order of how much noise it made:

Single sign-on. A large share of enterprise SSO uses SAML with the HTTP POST binding, in which the identity provider returns an HTML page that auto-submits a form back to the application. That is a cross-site top-level POST. Under the new default it arrives without the application’s cookies, and any state the application stored in a cookie before the redirect, such as the relay state or the return URL, is missing. OpenID Connect deployments using response_mode=form_post broke the same way for the same reason.

Embedded content. Payment forms, support chat widgets, video players and analytics tools hosted on a different site all load in frames, and every cookie they relied on was now cross-site. The fix was to add SameSite=None; Secure, which is a fix in the sense that it restores the old behaviour and none of the old protection.

Credentialed cross-origin requests. A background request from your page to an API on a different site, made with credentials included, is cross-site, so the cookie went missing while the CORS configuration remained unchanged and correct, which made the failure hard to diagnose.

And then the second-order problem, which is the part usually left out. The repair for all three cases was SameSite=None, and some clients could not parse it. Chrome’s own compatibility notes list three groups: Chrome versions 51 to 66 inclusive reject a cookie marked SameSite=None outright; versions of UC Browser on Android before 12.13.2 reject it; and, most awkwardly, Safari and embedded browsers on macOS 10.14 and all browsers on iOS 12 erroneously treat SameSite=None as though it were SameSite=Strict. Sites therefore had to sniff the user agent and send two different Set-Cookie headers to two populations, one with the attribute and one without, which is exactly the sort of fragile compatibility hack that security defaults are supposed to eliminate.

The lasting result is visible in the numbers. The Web Almanac’s July 2025 crawl found that among first-party cookies, 3% set SameSite=Strict, 19% set Lax, 11% set None, and 66% still set nothing at all and take the browser default. Nearly all third-party cookies set None, because they have no choice.

Prefixes: putting the attributes back into the name#

Because the Cookie header carries no attributes, a server cannot verify how a cookie was set. Cookie prefixes solve this by making the browser enforce a rule keyed on the name itself, so that the name becomes the evidence. They were proposed by Mike West, and shipped in Chrome 49 in 2016 after an intent to ship posted by Emily Stark in December 2015.

__Secure-. If a cookie’s name begins with a case-sensitive match for __Secure-, the cookie will have been set with the Secure attribute. A browser rejects Set-Cookie: __Secure-SID=12345; Domain=site.example and accepts Set-Cookie: __Secure-SID=12345; Domain=site.example; Secure when it comes from a secure origin.

__Host-. If a cookie’s name begins with __Host-, the cookie will have been set with Secure, with Path=/, and with no Domain attribute. The draft explains the point in one sentence worth memorizing: this combination yields a cookie that hews as closely as a cookie can to treating the origin as a security boundary. No Domain means the host-only flag is true, so no subdomain receives it and no subdomain can overwrite it. Path=/ means it cannot be shadowed by a cookie scoped to a narrower path. Secure means an insecure origin cannot touch it.

In 2025 two more prefixes arrived, addressing the gap that HttpOnly leaves in server-side reasoning. __Http- requires Secure and HttpOnly, which together prove the cookie was set by a Set-Cookie header and not by script through document.cookie or the Cookie Store API. __Host-Http- requires all of that plus the __Host- conditions. Chrome shipped them in Chrome 140 on 2 September 2025, following an intent to implement and ship posted by Yoav Weiss in June 2025; Firefox shipped them in version 142 in August 2025, without listing them in its release notes [UNVERIFIED: the exact Firefox version and date for the Http cookie prefixes, which is reported by the security press rather than by Mozilla’s own notes]. The motivation given was that servers should be able to tell that a cookie they received came from their own back end rather than from a confused developer’s script, a malicious browser extension, or an XSS payload.

Prefix What it guarantees Since
__Secure- Secure attribute set Chrome 49, 2016
__Host- Secure, Path=/, no Domain Chrome 49, 2016
__Http- Secure and HttpOnly Chrome 140, 2025
__Host-Http- Both of the above Chrome 140, 2025

The prefixes are cheap, they need no server support beyond changing a string, and they are almost entirely unused. The Web Almanac’s 2025 cookies chapter records that under 1% of first-party and under 1% of third-party cookies use __Host- or __Secure-, and notes that adoption has stayed very low in the ten years since they were introduced. If you take one action from this chapter, rename your session cookie.

Partitioned cookies, CHIPS, and the third-party endgame#

A cookie set by an embedded third party has, historically, been one cookie shared across every site that embeds it. That is the mechanism of cross-site tracking, and it is also the mechanism behind a great deal of legitimate embedded functionality, which is why removing it took six years and ended in a reversal.

CHIPS, Cookies Having Independent Partitioned State, is the compromise. A third party opts a cookie into partitioned storage by adding the Partitioned attribute. The browser then keeps a separate cookie jar for that third party per top-level site. The chat widget embedded on northbank.example and the same widget embedded on othersite.example each get their own cookie; neither can see the other; the widget still works, and the join across sites is gone.

The requirements are strict. A partitioned cookie must be Secure. The proposal states that user agents may only accept Partitioned cookies whose SameSite attribute is None, which follows from their purpose. Path=/ and the __Host- prefix are recommended, giving the canonical form:

Set-Cookie: __Host-widget=abc123; Secure; Path=/;
 SameSite=None; Partitioned

Chrome’s implementation limits a partition to 180 cookies per partition and 10 KB per embedded site, which is an implementation detail rather than a standard. CHIPS itself is incubated in the W3C Privacy Community Group; as of August 2026 the Partitioned attribute is not part of the 6265bis text, where it is an unrecognized extension attribute.

Browser support arrived over two years: Chrome 114 on 30 May 2023, Firefox 131 on 1 October 2024, and Safari 18.4, announced by WebKit on 31 March 2025. By July 2025 the Web Almanac found nearly 9% of third-party cookies on the top million sites were partitioned.

The wider third-party cookie story, which every practitioner will be asked about, went like this:

Date Event
24 Mar 2020 Safari blocks all 3P cookies
14 Jun 2022 Firefox TCP on by default
4 Jan 2024 Chrome restricts for 1%
22 Jul 2024 Chrome switches to a prompt
22 Apr 2025 Chrome drops the prompt
17 Oct 2025 Privacy Sandbox wound down

Safari removed third-party cookies outright on 24 March 2020, in the WebKit release described by John Wilander in the post “Full Third-Party Cookie Blocking and More”, covering Safari 13.1 on macOS and iOS and iPadOS 13.4; the announcement’s key line is that cookies for cross-site resources are now blocked by default across the board. Firefox took a different route, partitioning rather than blocking: Total Cookie Protection became the default for all users worldwide on 14 June 2022, giving every site its own cookie jar so that a third party embedded on two sites gets two separate jars.

Chrome announced its intention to remove third-party cookies in January 2020 and moved the date repeatedly. On 14 December 2023 it announced Tracking Protection, restricting third-party cookies by default for 1% of users from 4 January 2024, described at the time as a milestone toward phasing them out for everyone in the second half of 2024. On 22 July 2024 Google changed course, saying that instead of deprecating third-party cookies it would introduce a new experience in Chrome letting people make an informed choice that applies across their browsing. On 22 April 2025 it dropped that too, saying it would maintain its current approach of offering users third-party cookie choice in existing Chrome settings and would not launch a standalone prompt. On 17 October 2025, Anthony Chavez announced the wind-down of most Privacy Sandbox technologies, retiring the Topics API, Protected Audience, Attribution Reporting, Private Aggregation, Related Website Sets, IP Protection and others, while explicitly retaining three: CHIPS, FedCM and Private State Tokens, on the grounds that CHIPS and FedCM improve cookie privacy and identity flows respectively and have seen broad adoption including support from other browsers.

As of August 2026, then: third-party cookies remain enabled by default in Chrome and are user-controllable in settings; they are blocked in Safari and partitioned in Firefox; and the surviving standard for embedded state is the Partitioned attribute. Industry estimates published in 2026 put the share of global web traffic using browsers that block or partition third-party cookies by default at roughly 17 to 20%, which is a market estimate rather than a measured figure, but it is enough to make the point: a meaningful slice of your customers already browse without usable third-party cookies regardless of what Chrome does, so building on partitioned cookies is not optional caution, it is the working assumption.

Cookies and CSRF: why SameSite is not a complete defence#

Cross-site request forgery is the attack the fourth rule addresses. Its mechanism is not theft. The browser attaches cookies based on the destination of a request, never on its origin, so any page anywhere can cause an authenticated request to your site. Security literature calls this ambient authority: the credential applies to every request in the ambient environment without anyone choosing it.

Here is the attack against Northbank in full. The bank exposes a transfer endpoint that accepts a form POST. The attacker publishes a page on evil.example containing this:

A page on evil.example holds one hidden form:

  action = https://www.northbank.example/transfer
  method = POST
  field  to     = 447192
  field  amount = 25000

plus one line of script that submits it as the page loads.

A logged-in customer visits that page for any reason. The form submits itself, and this is what arrives at the bank:

POST /transfer HTTP/1.1
Host: www.northbank.example
Origin: https://evil.example
Content-Type: application/x-www-form-urlencoded
Cookie: sid=9f2c7a1e4b6d80f35c9e2a7b41d8e0c6

to=447192&amount=25000

Two details in that request deserve attention. The Content-Type is the default for an ordinary form submission, which means the browser treats it as a simple request and never asks the bank for permission first, so there is no cross-origin preflight check to save you. And the Origin header does say evil.example, which is the hook that a server-side check can hang on. If the session cookie has no SameSite attribute and Chrome’s two-minute window does not apply, or if the cookie is SameSite=None, the cookie goes with it, the bank sees an authenticated customer requesting a transfer of 25,000 rupees, and it obeys. The attacker never sees the cookie, never sees the response, and does not need to.

With SameSite=Lax or Strict, that specific attack fails, because a cross-site POST is neither same-site nor a safe-method top-level navigation. This is why SameSite was worth the disruption of 2020. It is also why it is routinely over-trusted.

OWASP’s Cross-Site Request Forgery Prevention Cheat Sheet is unambiguous: SameSite is useful as a defence-in-depth control but it does not replace a proper CSRF defence in most deployments. Here are the reasons, each of which is a real deployment failure and not a theoretical one.

First, same-site is not same-origin. A cookie marked SameSite=Strict is still sent on requests originating from promo.northbank.example, because that is the same site. An attacker with a foothold on any subdomain, including one obtained by taking over a dangling DNS record pointing at a decommissioned cloud bucket, is inside your SameSite boundary entirely.

Second, Lax still permits cross-site top-level GET. Any endpoint that changes state on a GET is still forgeable from any page in the world with a link or a redirect. Many older applications have such endpoints, and “delete” links are the classic case.

Third, Chrome’s Lax-plus-POST window. A cookie without an explicit SameSite attribute is still sent on cross-site POST for two minutes after it is set. An attacker who can get the victim to visit the site and then their page in quick succession is inside that window, which is why omitting the attribute and trusting the default is not equivalent to setting Lax.

Fourth, coverage. SameSite is enforced by the browser. Older browsers, embedded web views in mobile applications and unusual clients may not enforce it at all. OWASP’s guidance notes that browser coverage is not universal.

Fifth, None is often unavoidable. If your application is embedded, or takes a cross-site POST from an identity provider as part of single sign-on, some cookies must be SameSite=None and those cookies have no protection from this attribute at all.

Sixth, redirects. Whether a request counts as cross-site through a chain of redirects has been a genuine specification argument, and browsers have differed. Firefox even carries a preference for allowing what it calls boomerang redirects. Any behaviour that varies between browsers cannot be your only control.

Seventh, cookie writing beats cookie reading. An attacker who can set a cookie on your domain, from a subdomain or from a plain-HTTP injection point, can defeat the naive double-submit pattern without ever reading anything, because they control both halves of the comparison.

The current OWASP recommendations, in order, are: use your framework’s built-in protection if it has one; otherwise implement the synchronizer token pattern, in which the server generates a per-session, secret, unpredictable token and requires it in every state-changing request. Where a stateless option is needed, use the signed double-submit cookie, which OWASP calls the most secure implementation of that pattern because the token is explicitly tied to the authenticated session with an HMAC using a server-side secret. The naive double-submit cookie, where the same random value is placed in a cookie and in a form field and merely compared, is documented for reference only, with the explicit warning that it is bypassable by an attacker who can write cookies on the target domain, whether through a vulnerable sibling subdomain, a DNS takeover, or plaintext-HTTP cookie injection on a cookie that lacks the __Host- prefix. Note the last clause: the prefix is part of the CSRF defence, not merely cookie hygiene.

Two further controls are recommended as defence in depth. Custom request headers suit API endpoints, because a cross-site request carrying a custom header is not a simple request and triggers a CORS preflight that the attacker’s page cannot satisfy. And checking Sec-Fetch-Site, the browser-set fetch metadata header whose values are same-origin, same-site, cross-site and none, requires only a small amount of server-side logic and is described by OWASP as having high browser coverage. The academic root of this approach is Adam Barth, Collin Jackson and John Mitchell’s 2008 paper “Robust Defenses for Cross-Site Request Forgery”, which introduced the Origin header for exactly this purpose.

The correct stance is layered: SameSite=Lax or Strict on the session cookie, __Host- on its name, a synchronizer or signed double-submit token on every state-changing request, and an origin or fetch-metadata check as a backstop. Remove any one and there is a documented attack that walks through the gap.

Request: POST https://www.northbank.example/transfer
Started by: a page on evil.example

  cookie in jar: __Host-sid
        |
        v
  host matches?  yes, exact host, no Domain set
        |
        v
  path matches?  yes, "/" prefixes "/transfer"
        |
        v
  secure needed? yes, and request is https
        |
        v
  same site?     evil.example vs northbank.example
                 -> cross-site
        |
        +-- SameSite=Strict -> cookie not sent
        +-- SameSite=Lax    -> not sent, POST not safe
        +-- SameSite=None   -> cookie sent, CSRF token
                               is now the only defence

Here is the header Northbank should send on successful login, and the defence of every part of it.

Set-Cookie: __Host-sid=9f2c7a1e4b6d80f35c9e2a7b41d8e0c6;
 Max-Age=1800;
 Secure;
 HttpOnly;
 Path=/;
 SameSite=Lax

__Host- in the name. This costs nothing and buys the strongest scoping the cookie model offers. The browser will refuse the cookie unless it is Secure, has Path=/ and has no Domain. The value to the server is that any cookie arriving under this name must have been set that way, so a session cookie planted by a compromised subdomain cannot masquerade as this one. Consider __Host-Http- instead if you have confirmed that your target browsers support it, since it additionally proves the cookie was not written by script.

sid as the rest of the name. Short, meaningless, and revealing nothing about the framework in use. Default names such as PHPSESSID, JSESSIONID or connect.sid tell an attacker what to attack.

The value. 32 hexadecimal characters is 128 bits of entropy (the amount of genuine unpredictability in a value) drawn from a cryptographically secure random source. What makes an identifier long enough is chapter 36’s subject; the relevant point here is that the value must be meaningless to the browser and unguessable to everyone.

Max-Age=1800. Thirty minutes, expressed in seconds. Max-Age is preferred to Expires because it is a duration and therefore immune to clock skew on the customer’s machine. This is browser-side housekeeping only. The authoritative expiry is the record on the server, which must be checked on every request regardless of what the browser sends.

Secure. Three guarantees rather than one: the cookie is never sent over a plain connection, it cannot be created by a plain-HTTP page, and it cannot be overwritten by one. The third is the reason a site that is entirely HTTPS still needs the attribute.

HttpOnly. Removes the cookie from document.cookie and from the Cookie Store API, so an injected script cannot exfiltrate the session. It does not stop that script from making requests as the customer, so it reduces the blast radius of XSS rather than eliminating it.

Path=/. Required by the __Host- prefix, and correct anyway, since Path is a filing rule with no security force.

SameSite=Lax. Blocks the cross-site POST that carries the forged transfer, and blocks cookie delivery to frames, images and background fetches on other sites, while still allowing a customer arriving by a link from their email to land logged in. Choose Strict if you are prepared for inbound links to show a logged-out page first, which for a bank is often the right trade. Never leave the attribute off and rely on the default: the default in Chrome is the more permissive Lax-plus-POST behaviour with its two-minute window, and defaults vary between browsers.

No Domain. Deliberately absent, so the cookie is host-only and promo.northbank.example never sees it. This is the single most valuable omission on the header.

The same site’s embedded support widget, hosted on a different domain and framed into pages, needs the opposite of most of this, and the correct form is:

Set-Cookie: __Host-chat=7c1b9ae0f4d2; Secure; Path=/;
 SameSite=None; Partitioned; HttpOnly

SameSite=None because it must work cross-site; Secure because None without it is discarded; Partitioned so the browser keeps a separate jar per top-level site, which keeps the widget working in Chrome without third-party tracking and keeps it working at all in Safari and Firefox.

Two changes are visible on the horizon and both tighten the model further. Chrome is binding cookies to the origin that set them, including scheme and port, with Chrome Platform Status recording that from Chrome 148 cookies are bound to their setting origin by default, so that a cookie set by one origin is no longer visible to another origin on the same host [UNVERIFIED: whether the Chrome 148 origin-binding default had completed its stable rollout by August 2026]. And Device Bound Session Credentials, which ties a session to a private key held in the device’s secure hardware and forces the browser to refresh a short-lived cookie by signing a challenge, reached general availability on Windows in Chrome 146 in April 2026, with macOS support announced for a later release. The second is the first serious attempt to make a stolen cookie useless on another machine, which is the one problem no attribute in this chapter can solve.

37.98 Common wrong ideas#

Wrong: Domain narrows a cookie to a particular host. Right: Domain only ever widens. Omitting it makes the cookie host-only, sent to the exact host that set it; setting Domain=northbank.example sends it to that domain and every subdomain beneath it, and also allows any of those subdomains to overwrite it.

Wrong: A Secure cookie is safe because it is only sent over HTTPS. Right: Secure does three things: it withholds the cookie from non-secure requests, it prevents a non-secure page from creating it, and it prevents a non-secure page from overwriting an existing one. It does not encrypt anything, does not hide the cookie from script, and does not stop it being sent cross-site.

Wrong: HttpOnly protects a session from cross-site scripting. Right: HttpOnly stops script reading the cookie, which prevents exfiltration, but an attacker with script execution on your origin can simply issue requests from the page and the browser will attach the cookie. It converts theft into use, which limits the damage without removing it.

Wrong: SameSite=Strict makes cross-site request forgery impossible. Right: SameSite operates on sites, not origins, so any compromised subdomain is inside the boundary; browsers vary on redirect chains; older and embedded clients may not enforce it; and cookies that must be SameSite=None get no protection at all. OWASP treats it as defence in depth alongside a synchronizer or signed double-submit token.

Wrong: Omitting SameSite is the same as setting SameSite=Lax, since Lax is the default now. Right: In Chrome the default is a more permissive variant that still sends the cookie on cross-site top-level POSTs for two minutes after it is set. Setting the attribute explicitly removes that window and removes any dependence on which browser the customer is using.

Wrong: The server can check how a cookie was set by inspecting the request. Right: The Cookie header carries only name and value pairs. No attribute, no originating host, no indication of which page set it. That gap is precisely why the __Secure-, __Host-, __Http- and __Host-Http- prefixes exist: they move the guarantee into the name, which is the only part the server can see.

Wrong: Path isolates cookies between different areas of a site. Right: Path decides which requests carry the cookie, but pages on the same host are same-origin regardless of path, so a script on one path can open a frame on another and read its cookies. The 6265bis draft states that Path cannot be relied upon for security.

Wrong: Third-party cookies were removed from the web in 2024. Right: Safari blocked them on 24 March 2020 and Firefox partitioned them by default on 14 June 2022, but Chrome reversed its plan on 22 July 2024, dropped the replacement choice prompt on 22 April 2025, and wound down most of Privacy Sandbox on 17 October 2025 while keeping CHIPS. As of August 2026 third-party cookies remain enabled by default in Chrome.

Wrong: Setting an expiry in the past reliably deletes a cookie. Right: A deletion only matches if the Domain and Path are identical to the original. Otherwise the browser stores a second cookie with the same name and different scope, and then sends both, which produces failures that are extremely hard to diagnose. Server-side invalidation, covered in chapter 36, is what actually ends a session.

Wrong: A cookie will live as long as its expiry date says. Right: Chrome has capped cookie lifetime at 400 days since version 104 in August 2022, and 6265bis recommends the same figure; Safari caps script-written cookies at seven days; and browsers evict cookies when storage limits are reached or the user clears data.

37.99 Chapter summary in 20 lines#

  1. A cookie is a name and a value that a browser stores on a server’s instruction and returns automatically on later requests to that server.
  2. The cookie itself carries no security properties, so everything that makes a session safe lives in the attributes attached to the Set-Cookie header.
  3. Every attribute in the specification was added after a specific class of attack became too expensive to tolerate, which is why the list reads like a history of web security.
  4. Attributes travel only from server to browser; the returning Cookie header contains bare name and value pairs and nothing else.
  5. Because of that asymmetry, a server cannot verify how any cookie it receives was created, and this is the entire reason cookie name prefixes exist.
  6. Omitting Domain makes a cookie host-only, while setting it widens the cookie to that domain and every subdomain below it; the attribute has no narrowing form.
  7. Any subdomain can set a cookie for its parent domain, so a compromised marketing host can plant a session cookie that the main application cannot distinguish from its own.
  8. Path restricts which requests carry a cookie but provides no isolation, because same-host pages are same-origin and can read each other’s cookies through a frame.
  9. Secure withholds a cookie from non-secure requests, prevents a non-secure page from creating it, and prevents a non-secure page from overwriting a secure one.
  10. HttpOnly hides a cookie from document.cookie and the Cookie Store API, turning cookie theft into cookie use rather than preventing abuse outright.
  11. SameSite=Strict sends the cookie on no cross-site request at all, Lax adds cross-site top-level navigations using the safe methods GET, HEAD, OPTIONS and TRACE, and None sends it everywhere.
  12. SameSite=None is ignored entirely unless the cookie is also Secure, so the two must always be written together.
  13. Chrome made Lax the default from Chrome 80 in February 2020, rolled it back on 3 April 2020 because of COVID-19, resumed on 14 July 2020 and reached full rollout on 11 August 2020.
  14. The change broke SAML POST-binding single sign-on, OpenID Connect form-post responses, embedded payment and chat widgets, and credentialed cross-site API calls, and the repair, SameSite=None, was itself rejected or mishandled by Chrome 51 to 66, older UC Browser and everything on macOS 10.14 and iOS 12.
  15. __Secure- proves the Secure attribute was set, and __Host- additionally proves Path=/ and the absence of Domain, which is as close as a cookie can come to being scoped to an origin.
  16. Chrome 140 in September 2025 added __Http- and __Host-Http-, which prove a cookie was set by a header rather than by script, yet prefix adoption across the web remains under 1%.
  17. The Partitioned attribute, from the CHIPS proposal, gives an embedded third party a separate cookie jar for each top-level site, and requires Secure and SameSite=None.
  18. As of August 2026 third-party cookies are blocked in Safari since 24 March 2020, partitioned in Firefox since 14 June 2022, and still enabled by default in Chrome after the reversals of July 2024 and April 2025 and the Privacy Sandbox wind-down of 17 October 2025.
  19. SameSite is not a complete CSRF defence because it works at site rather than origin granularity, permits cross-site GET under Lax, carries a two-minute POST exception in Chrome, and cannot apply to cookies that must be SameSite=None.
  20. A hardened login cookie names itself __Host-, carries a long random value, and sets Max-Age, Secure, HttpOnly, Path=/ and an explicit SameSite, with no Domain, backed by a server-side token check on every state-changing request.

Chapter sources: RFC 6265, “HTTP State Management Mechanism”, Adam Barth, April 2011, sections 5.1.3, 5.1.4 and 6.1; draft-ietf-httpbis-rfc6265bis-22, Steven Bingler, Mike West and John Wilander, 1 December 2025, in the RFC Editor queue at the time of writing, sections 4.1.1, 4.1.3, 4.2.1, 5.5, 5.6.1 to 5.6.7 and the storage model steps on the secure-only and http-only flags; draft-ietf-httpbis-layered-cookies-02, Anne van Kesteren and Johann Hofmann, 21 May 2026; RFC 2109, February 1997, by David Kristol and Lou Montulli, and RFC 2965, October 2000, for the superseded cookie specifications; United States patent 5,774,670, applied for by Lou Montulli in 1995 and granted in 1998, and the Financial Times article of 12 February 1996 that first made cookies public, for the invention history; RFC 9110 section 9.2.1 for the definition of safe methods; the Chromium SameSite updates page for the rollout timeline of 4 February, 17 February, 3 April, 14 July and 11 August 2020, and the Chromium blog posts “Temporarily rolling back SameSite Cookie Changes”, 3 April 2020, and “Resuming SameSite Cookie Changes in July”, 28 May 2020; the Chromium “SameSite=None: Known Incompatible Clients” page for Chrome 51 to 66, UC Browser before 12.13.2 and macOS 10.14 and iOS 12 behaviour; the Chrome for Developers post “Cookie Expires and Max-Age attributes now have upper limit” for the 400-day cap from Chrome 104 in August 2022; the Chrome 140 release notes of 2 September 2025 and the blink-dev intent to implement and ship posted by Yoav Weiss in June 2025 for the __Http- and __Host-Http- prefixes; the blink-dev intent to ship for cookie prefixes posted by Emily Stark on 8 December 2015 and the Chrome samples page recording Chrome 49 support; the Privacy Sandbox CHIPS documentation and the privacycg CHIPS proposal for partitioned cookie requirements and limits; “New in Chrome 114”, 30 May 2023, the MDN Firefox 131 release notes of 1 October 2024, and “WebKit Features in Safari 18.4”, 31 March 2025, for partitioned cookie support; John Wilander, “Full Third-Party Cookie Blocking and More”, WebKit, 24 March 2020; the Mozilla blog announcement of Total Cookie Protection by default, 14 June 2022; the Google blog post announcing Tracking Protection, 14 December 2023, for the 1% restriction from 4 January 2024; the Privacy Sandbox posts of 22 July 2024, 22 April 2025 and Anthony Chavez’s update of 17 October 2025 on the retirement of Privacy Sandbox technologies and the retention of CHIPS, FedCM and Private State Tokens; the OWASP Cross-Site Request Forgery Prevention Cheat Sheet for the synchronizer token, signed double-submit, naive double-submit warning, custom header and Sec-Fetch-Site guidance; the OWASP HttpOnly community page for the Internet Explorer 6 SP1 origin in 2002; Adam Barth, Collin Jackson and John C. Mitchell, “Robust Defenses for Cross-Site Request Forgery”, ACM CCS 2008; Xiaofeng Zheng, Jian Jiang, Jinjin Liang, Haixin Duan, Shuo Chen, Tao Wan and Nicholas Weaver, “Cookies Lack Integrity: Real-World Implications”, 24th USENIX Security Symposium, 2015; the HTTP Archive Web Almanac 2025 cookies chapter, based on the July 2025 crawl of the top one million sites, for attribute adoption, median cookie size of 40 bytes and median expiry of one year; the Google security blog post “Protecting Cookies with Device Bound Session Credentials”, 9 April 2026, for general availability on Windows in Chrome 146; and the Chrome Platform Status entry “Origin-Bound cookies (by default)” for the Chrome 148 change.