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
Domainmeans host-only. WithDomain=shopa.example, the host setting it must beshopa.exampleor a subdomain such aswww.shopa.example. ShopA cannot set a cookie foradco.example, and nobody can set one forexample. - Path. Without
Path, the cookie gets the folder of the address: set from/account/settings, it belongs to/account. - Security.
Securecookies only come from HTTPS.SameSite=NoneandPartitionedboth requireSecure. A name starting__Secure-needsSecure;__Host-also needs host-only andPath=/, which pins it to one exact host. - Expiry.
Max-Agewins overExpires. 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/xmatches/account, but/accountsdoes not. Securecookies 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-levelGETnavigation).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 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.
This lesson is locked
Lessons open one at a time. Finish the previous lesson to unlock this one.