Syllabus Lesson 36 of 156 · HTTP from the inside
HTTP from the inside

A cookie jar that follows the rules

Why it matters Every rule your browser applies to cookies is a privacy or security decision: which site may read a cookie, whether it follows you across sites, when it dies. Writing the jar yourself is the best way to see that "third-party cookie" is not magic, just a few checks, and that a browser can switch tracking on or off by changing them.

A cookie jar is the browser's drawer of cloakroom tickets. Two jobs: when a reply arrives with Set-Cookie, decide whether to keep the ticket and how to file it; when a request goes out, decide which tickets to show. Each rule below is one question the drawer asks.

Cookie jar: the browser's store of cookies, with the rules for adding them and for choosing which ones to send.

Host-only cookie: a cookie set without Domain. It goes back only to the exact host that set it, not to its subdomains.

Public suffix: an ending under which anyone can register names, such as .com or .co.uk, or .example in the practice world (watched.urls.PUBLIC_SUFFIXES). A cookie for a whole public suffix would be shared by every site, so it is refused.

Storing: should I keep this ticket?

  • Domain. No Domain means host-only. With Domain=shopa.example, the host setting it must be shopa.example or a subdomain such as www.shopa.example. ShopA cannot set a cookie for adco.example, and nobody can set one for example.
  • Path. Without Path, the cookie gets the folder of the address: set from /account/settings, it belongs to /account.
  • Security. Secure cookies only come from HTTPS. SameSite=None and Partitioned both require Secure. A name starting __Secure- needs Secure; __Host- also needs host-only and Path=/, which pins it to one exact host.
  • Expiry. Max-Age wins over Expires. No expiry at all makes a session cookie, forgotten when the browser closes. An expiry in the past is how a server deletes a cookie.
  • Replacing. Same name, domain, path and partition: the new cookie replaces the old one.

Sending: which tickets do I show?

  • The host must match (exactly for host-only, or as a subdomain for Domain cookies), and the path must match: /account/x matches /account, but /accounts does not.
  • Secure cookies go only over HTTPS. Expired ones are thrown away.
  • SameSite applies when the request is cross-site, that is, the page making it is on another site. Strict: never. Lax: only when you click a link to the site (a top-level GET navigation). None: always. A cookie with no SameSite is treated as Lax, as Chrome does by default (checked October 2026).
  • Partitioned cookies are filed under the top-level site where they were set, and sent only under that same site. That is CHIPS, from Leg 1: the chat widget works on ShopA, but its ShopA cookie never shows up on ShopB.
  • Order: cookies with longer paths first, then oldest first.

The cases this lesson is graded on are modelled on the rules in draft-ietf-httpbis-rfc6265bis, the updated cookie standard, approved and awaiting its RFC number (checked October 2026). We leave out a few real-browser details: size limits, a non-HTTPS site being unable to overwrite a Secure cookie, and a cross-site subresource being unable to set Lax or Strict cookies.

Cross-site request: a request whose destination is on a different site from the page that sent it, such as ShopA's page loading adco.example/p.gif. Third-party tracking cookies live in exactly these requests.

A common misconception

"Domain=shopa.example makes a cookie safer." It does the opposite: it widens the cookie to every subdomain. Leaving Domain out (host-only), or using the __Host- prefix, is the tighter choice.

Try it In DevTools (Application or Storage, then Cookies), find a cookie with SameSite set to None. Then visit that site's page from a link on another site and look at the Network tab: does the cookie go on the image and script requests too? That is a cross-site cookie at work.

In other languages

JavaScript:

function pathMatch(reqPath, cookiePath) {
  if (reqPath === cookiePath) return true;
  return reqPath.startsWith(cookiePath) &&
    (cookiePath.endsWith("/") || reqPath[cookiePath.length] === "/");
}
function sameSiteAllows(mode, crossSite, navigation, method) {
  if (!crossSite) return true;
  mode = mode || "Lax";
  if (mode === "Strict") return false;
  if (mode === "Lax") return navigation && method === "GET";
  return true;
}

Go:

// net/http/cookiejar is a full jar; give it a public suffix list
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
client := &http.Client{Jar: jar}

Rust:

fn domain_match(host: &str, domain: &str) -> bool {
    host == domain || host.ends_with(&format!(".{domain}"))
}

Recap

  • Storing checks domain (no public suffixes, no other sites), path, Secure, prefixes, SameSite=None and Partitioned needing Secure, and expiry.
  • Sending checks host, path, Secure and expiry, then SameSite for cross-site requests: Strict never, Lax only on top-level GET navigations, None always.
  • Partitioned cookies are filed under the top-level site and only sent there, which is what stops them following you.
  • Max-Age wins over Expires, and an expiry in the past deletes the cookie.
Your turn

Your parse_set_cookie is given. Write domain_match, default_path and path_match, then store(jar, line, url, now, top_site=None), which applies one Set-Cookie line to the list jar and returns the stored cookie dict (keys name, value, domain, host_only, path, expires, secure, httponly, samesite, partition) or None, and cookie_header(jar, url, now, top_site=None, navigation=False, method="GET"), which returns the Cookie value to send. Follow the rules in the starter's docstrings exactly.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output