<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Cookies · XB Field Notes</title><link>https://www.xbcatm.com/tags/cookies/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/cookies/index.xml" rel="self" type="application/rss+xml"/><item><title>Cookie scope keeps the session where it belongs</title><link>https://www.xbcatm.com/posts/cookie-scope-keeps-the-session-small/</link><pubDate>Sun, 25 Oct 2026 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/cookie-scope-keeps-the-session-small/</guid><description>A cookie sent everywhere is a bigger surface to lose. Path, domain, secure, and SameSite keep it attached to the request that actually needs it.</description><content:encoded>&lt;p&gt;A cookie&amp;rsquo;s scope is the set of requests that carry it. Domain, Path, Secure and SameSite each remove requests from that set, and the browser applies all of them before writing a Cookie header. Scope is the cheapest control in a web stack: free at request time, and it shrinks how often a session token crosses a network you do not own.&lt;/p&gt;
&lt;p&gt;Convenience pushes the other way. A shared login across www, api and shop invites &lt;code&gt;Domain=example.com&lt;/code&gt;; a single-page app under &lt;code&gt;/app&lt;/code&gt; invites &lt;code&gt;Path=/&lt;/code&gt;; a page that needs the session for one banner invites &lt;code&gt;SameSite=None&lt;/code&gt;. Each widening is defensible alone, and each adds hosts or paths that can now read, replay or overwrite the token.&lt;/p&gt;
&lt;h2 id="domain-is-a-suffix-match-not-a-wildcard"&gt;Domain is a suffix match, not a wildcard&lt;/h2&gt;
&lt;p&gt;RFC 6265 is blunt: with &lt;code&gt;Domain=example.com&lt;/code&gt; the user agent sends the cookie to example.com, &lt;a href="https://www.example.com"&gt;www.example.com&lt;/a&gt; and &lt;a href="https://www.corp.example.com"&gt;www.corp.example.com&lt;/a&gt;. A leading dot is ignored — &amp;ldquo;even though that character is not permitted&amp;rdquo; — so the dotted and undotted spellings are the same cookie. The dot never was a wildcard. The fixture shows the split: Python&amp;rsquo;s http.cookies keeps &lt;code&gt;.example.com&lt;/code&gt; verbatim while the RFC&amp;rsquo;s domain-match algorithm strips it, so a server that keys scope logic on the raw header disagrees with the browser about the same cookie.&lt;/p&gt;
&lt;p&gt;Omit Domain and you get a host-only cookie, returned &amp;ldquo;only to the origin server&amp;rdquo; — the narrow default, and the one you want. The wide version hands the session to every sibling subdomain, including ones nobody thinks of as the app: a status-page host, a docs host, a CMS marketing owns. The fixture&amp;rsquo;s table shows &lt;code&gt;Domain=example.com&lt;/code&gt; attaching to &lt;code&gt;evil.example.com&lt;/code&gt;, while a host-only cookie from &lt;code&gt;www.example.com&lt;/code&gt; never reaches &lt;code&gt;shop.example.com&lt;/code&gt;. A Domain attribute must domain-match the setting host, so widening goes upward only: &lt;code&gt;Domain=shop.example.com&lt;/code&gt; from &lt;code&gt;www.example.com&lt;/code&gt; is rejected.&lt;/p&gt;
&lt;h2 id="path-is-a-prefix-not-a-directory"&gt;Path is a prefix, not a directory&lt;/h2&gt;
&lt;p&gt;Path looks like a directory boundary and is not one. RFC 6265 defines path-match as three cases: identical paths, or the cookie path is a prefix ending in &lt;code&gt;/&lt;/code&gt;, or the next character of the request path is &lt;code&gt;/&lt;/code&gt;. &lt;code&gt;Path=/admin&lt;/code&gt; matches &lt;code&gt;/admin&lt;/code&gt;, &lt;code&gt;/admin/&lt;/code&gt; and &lt;code&gt;/admin/users&lt;/code&gt;, but not &lt;code&gt;/adminx&lt;/code&gt; or &lt;code&gt;/administrator&lt;/code&gt; — the fixture walks that table. &lt;code&gt;Path=/&lt;/code&gt; is the default when the attribute is omitted, which is why &amp;ldquo;we set a path&amp;rdquo; often means nothing.&lt;/p&gt;
&lt;p&gt;Path is not a security boundary, and the RFC says so: &amp;ldquo;the Path attribute cannot be relied upon for security&amp;rdquo;. The cookie still leaves the host on every matching request, and any script on an overlapping path reads it without HttpOnly. Treat Path as a request-count optimisation, never as an authorisation check.&lt;/p&gt;
&lt;h2 id="prefixes-enforce-what-attributes-cannot"&gt;Prefixes enforce what attributes cannot&lt;/h2&gt;
&lt;p&gt;Attributes are declarations; the two cookie-name prefixes are requirements the browser enforces at set time. &lt;code&gt;__Secure-&lt;/code&gt; demands Secure and a secure origin. &lt;code&gt;__Host-&lt;/code&gt; demands Secure, &lt;code&gt;Path=/&lt;/code&gt; and no Domain attribute, and the draft calls the result as close as a cookie can get to treating the origin as a security boundary. A &lt;code&gt;__Host-&lt;/code&gt; cookie is host-only by construction, so no sibling subdomain can set, overwrite or read it.&lt;/p&gt;
&lt;p&gt;That makes it a session-fixation defence, not a nicety. Fixation needs the attacker to choose the victim&amp;rsquo;s session id, and any host in the cookie&amp;rsquo;s domain can do that for a Domain-scoped cookie — as can a network attacker when Secure is missing, because a plaintext response can set a cookie the browser later sends over HTTPS. &lt;code&gt;__Host-session=…; Path=/; Secure; HttpOnly; SameSite=Lax&lt;/code&gt; closes both.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The classic failure is the parent-domain session cookie, set because www and api both need it. It presents as a session token in the access log of a host that should never have seen it: a static-asset domain, a CDN, a compromised marketing subdomain. Diagnose by reading the Set-Cookie line on the login response and asking which hosts under that domain could set a matching cookie; if one should not read a session, Domain is the bug.&lt;/p&gt;
&lt;p&gt;The second failure is fixation through a too-wide scope. An attacker who controls any host inside the cookie&amp;rsquo;s domain, or who can inject one plaintext response, plants a known session id before the victim logs in. The victim authenticates, the server binds that id to the account, and the attacker holds an authenticated session without ever seeing a credential. The blast radius is an account takeover with no authentication anomaly anywhere in the logs.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;p&gt;Read the scope the server actually sets, then try to use it from a host that should not have it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;curl -sD - -o /dev/null https://app.example.com/login | grep -i &amp;#39;^set-cookie:&amp;#39;
curl -s -c /tmp/cj -o /dev/null https://app.example.com/login
grep -i &amp;#39;session\|__Host&amp;#39; /tmp/cj
curl -sv -b /tmp/cj -o /dev/null https://shop.example.com/ 2&amp;gt;&amp;amp;1 | grep -i &amp;#39;^&amp;gt; cookie&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first line should show a &lt;code&gt;__Host-&lt;/code&gt; name with &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;SameSite=Lax&lt;/code&gt; and no Domain attribute, and the last should print nothing: a host-only cookie has no business on a sibling host. A &lt;code&gt;Domain=&lt;/code&gt; in the first output, or a Cookie header in the last, is the leak: the same token is now readable by every host under that suffix.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc6265.html"&gt;RFC 6265: HTTP State Management Mechanism&lt;/a&gt;, domain-match, path-match and the leading-dot rule, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie"&gt;MDN: Set-Cookie&lt;/a&gt;, attributes and the &lt;code&gt;__Host-&lt;/code&gt; / &lt;code&gt;__Secure-&lt;/code&gt; prefixes, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies"&gt;MDN: Using HTTP cookies&lt;/a&gt;, scope, host-only cookies and prefixes in practice, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>