<?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/" xmlns:blog="https://jonesrussell.github.io/blog/ns"><channel><title>Cookies on Web Developer Blog</title><link>https://jonesrussell.github.io/blog/tags/cookies/</link><description>Recent content in Cookies on Web Developer Blog</description><image><title>Web Developer Blog</title><url>https://jonesrussell.github.io/blog/images/og-default.png</url><link>https://jonesrussell.github.io/blog/images/og-default.png</link></image><generator>Hugo -- 0.166.0</generator><language>en-us</language><lastBuildDate>Sat, 19 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jonesrussell.github.io/blog/tags/cookies/feed.xml" rel="self" type="application/rss+xml"/><item><title>Host-bound session and CSRF cookies in Waaseyaa</title><link>https://jonesrussell.github.io/blog/host-bound-session-csrf-cookies/</link><pubDate>Sat, 19 Sep 2026 00:00:00 +0000</pubDate><guid>https://jonesrussell.github.io/blog/host-bound-session-csrf-cookies/</guid><category>php</category><category>waaseyaa</category><blog:tag>php</blog:tag><blog:tag>waaseyaa</blog:tag><blog:tag>security</blog:tag><blog:tag>cookies</blog:tag><description>How Waaseyaa&amp;rsquo;s SessionCookiePolicy enforces the __Host- cookie prefix&amp;rsquo;s four constraints for both the session cookie and the CSRF double-submit cookie, and rejects misconfiguration at boot instead of failing silently in the browser.</description><content:encoded><![CDATA[<p>Ahnii!</p>
<p>The <code>__Host-</code> cookie prefix is one of the stronger security guarantees browsers give you: a cookie named <code>__Host-something</code> can only be set and read by the exact origin that set it, no matter how many subdomains share the parent domain. That closes a real attack: a compromised or malicious subdomain writing a cookie that your main app then trusts as its own session.</p>
<p>The catch is that <code>__Host-</code> isn&rsquo;t a flag you flip. It&rsquo;s a contract with four parts, and if your server violates any one of them, the browser doesn&rsquo;t reject the request or throw an error — it just silently drops the cookie. <a href="https://github.com/waaseyaa/framework">Waaseyaa</a> recently added a <code>host_bound</code> mode to its <code>SessionCookiePolicy</code> that enforces the full contract for both the session cookie and the CSRF cookie, and refuses to boot if the configuration can&rsquo;t satisfy it. What follows is the four-part contract, how <code>host_bound</code> enforces it for both cookies, and where it refuses to boot instead.</p>
<h2 id="the-four-constraints">The Four Constraints</h2>
<p>A cookie name starting with <code>__Host-</code> is only valid if all of these hold at once:</p>
<ul>
<li><strong>No <code>Domain</code> attribute.</strong> The cookie must be host-only — omitting <code>Domain</code> scopes it to the exact host, not <code>Domain=example.com</code> which would let subdomains see it too.</li>
<li><strong><code>Path=/</code>.</strong> Any narrower path is rejected.</li>
<li><strong><code>Secure</code> is set.</strong> The cookie only travels over HTTPS.</li>
<li><strong>Secure context, implicitly.</strong> The cookie has to originate from a secure context in the first place — <code>Secure</code> alone doesn&rsquo;t retroactively fix an insecure origin.</li>
</ul>
<p>Get any of these wrong and the browser doesn&rsquo;t error — it drops the <code>Set-Cookie</code> header for that cookie entirely. Your session cookie silently never gets set, a user hits a login loop, and there&rsquo;s no log line pointing at the cause. That&rsquo;s the gap Waaseyaa&rsquo;s <code>host_bound</code> option closes: instead of trusting config to be correct, it validates the contract at construction time.</p>
<h2 id="one-policy-two-cookies">One Policy, Two Cookies</h2>
<p>Waaseyaa mints two cookies that need the same hardening:</p>
<ul>
<li><strong>The PHP session cookie.</strong></li>
<li><strong>The CSRF double-submit cookie</strong> (<code>XSRF-TOKEN</code> by default), read by <a href="https://inertiajs.com/">Inertia</a>&rsquo;s axios adapter and forwarded as the <code>X-XSRF-TOKEN</code> header on every mutation.</li>
</ul>
<p>Before this change, <code>CsrfMiddleware</code> hard-coded the <code>XSRF-TOKEN</code> name. Now both cookies are governed by the same <code>session.cookie</code> config, resolved through <code>SessionCookiePolicy</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">final</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SessionCookiePolicy</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">DEFAULT_CSRF_COOKIE_NAME</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;XSRF-TOKEN&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">HOST_BOUND_SESSION_COOKIE_NAME</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;__Host-waaseyaa_session&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">HOST_BOUND_CSRF_COOKIE_NAME</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;__Host-XSRF-TOKEN&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">const</span> <span style="color:#66d9ef">array</span> <span style="color:#a6e22e">SECURE_COOKIE_DEFAULTS</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;httponly&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;secure&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;auto&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;samesite&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Lax&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;use_strict_mode&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;path&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;/&#39;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;csrf_name&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">self</span><span style="color:#f92672">::</span><span style="color:#a6e22e">DEFAULT_CSRF_COOKIE_NAME</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;host_bound&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Setting <code>session.cookie.host_bound =&gt; true</code> switches both cookies to the <code>__Host-</code> profile:</p>
<table>
	<thead>
			<tr>
					<th>Attribute</th>
					<th>Default</th>
					<th>Host-bound</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>Secure</code></td>
					<td>Follows the <code>secure</code> setting (<code>'auto'</code>)</td>
					<td>Forced on, regardless of <code>secure</code></td>
			</tr>
			<tr>
					<td><code>Path</code></td>
					<td>Configurable</td>
					<td>Forced to <code>/</code></td>
			</tr>
			<tr>
					<td><code>Domain</code></td>
					<td>Configurable</td>
					<td>Forced unset</td>
			</tr>
			<tr>
					<td>Cookie name</td>
					<td><code>waaseyaa_session</code> / <code>XSRF-TOKEN</code></td>
					<td><code>__Host-waaseyaa_session</code> / <code>__Host-XSRF-TOKEN</code> (or an explicit <code>__Host-</code>-prefixed override)</td>
			</tr>
	</tbody>
</table>
<p><code>SessionMiddleware</code> applies the resolved policy to the PHP session cookie ini. <code>CsrfMiddleware</code> applies the same policy object to the CSRF cookie. Same source, so the two cookies can&rsquo;t drift out of sync.</p>
<h2 id="rejected-at-construction-not-discovered-in-the-browser">Rejected at Construction, Not Discovered in the Browser</h2>
<p>The policy validates the whole contract when it&rsquo;s built, before a single request is handled:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">assertConfigurationCompatible</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">void</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>$this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">hostBound</span>()) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// non-host-bound cookie name validation
</span></span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $path <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">path</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ($path <span style="color:#f92672">!==</span> <span style="color:#e6db74">&#39;/&#39;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">InvalidSessionCookiePolicyException</span>(<span style="color:#a6e22e">sprintf</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Host-bound session cookies require path &#34;/&#34;, got &#34;%s&#34;.&#39;</span>,
</span></span><span style="display:flex;"><span>            $path,
</span></span><span style="display:flex;"><span>        ));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $domain <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">options</span>[<span style="color:#e6db74">&#39;domain&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#66d9ef">null</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">is_string</span>($domain) <span style="color:#f92672">&amp;&amp;</span> $domain <span style="color:#f92672">!==</span> <span style="color:#e6db74">&#39;&#39;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">InvalidSessionCookiePolicyException</span>(<span style="color:#a6e22e">sprintf</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Host-bound session cookies must omit Domain; got &#34;%s&#34;.&#39;</span>,
</span></span><span style="display:flex;"><span>            $domain,
</span></span><span style="display:flex;"><span>        ));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $secure <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">options</span>[<span style="color:#e6db74">&#39;secure&#39;</span>];
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ($secure <span style="color:#f92672">!==</span> <span style="color:#e6db74">&#39;auto&#39;</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#f92672">!</span><span style="color:#a6e22e">filter_var</span>($secure, <span style="color:#a6e22e">FILTER_VALIDATE_BOOLEAN</span>)) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">InvalidSessionCookiePolicyException</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Host-bound session cookies require secure=true (or auto); secure=false is incompatible.&#39;</span>,
</span></span><span style="display:flex;"><span>        );
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    $sessionName <span style="color:#f92672">=</span> $this<span style="color:#f92672">-&gt;</span><span style="color:#a6e22e">sessionName</span>();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ($sessionName <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">||</span> <span style="color:#f92672">!</span><span style="color:#a6e22e">str_starts_with</span>($sessionName, <span style="color:#e6db74">&#39;__Host-&#39;</span>)) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">InvalidSessionCookiePolicyException</span>(<span style="color:#a6e22e">sprintf</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;Host-bound session cookie name must use the __Host- prefix; got &#34;%s&#34;.&#39;</span>,
</span></span><span style="display:flex;"><span>            (<span style="color:#a6e22e">string</span>) $sessionName,
</span></span><span style="display:flex;"><span>        ));
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// same check repeated for the CSRF cookie name
</span></span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>If you set <code>host_bound =&gt; true</code> and also set an explicit <code>Domain</code>, or a non-root <code>Path</code>, or <code>secure =&gt; false</code>, the app refuses to start. That trade-off is deliberate: a config error that fails loudly in CI or on deploy is recoverable. A config error that fails silently in production means some users can&rsquo;t log in, and there&rsquo;s no exception anywhere to explain why.</p>
<p>The policy also checks <strong>already-active</strong> PHP sessions, not just fresh config. If something upstream (another bootstrap path, an inherited <code>php.ini</code>) already started a session before the policy runs, <code>assertCompatibleWithActiveSession()</code> compares the live <code>session_name()</code> and <code>session_get_cookie_params()</code> against the resolved policy and throws if they disagree — catching the case where host-bound mode is configured but a prestarted session is still running under the old, non-<code>__Host-</code> name.</p>
<h2 id="malformed-config-is-rejected-too">Malformed Config Is Rejected Too</h2>
<p>Beyond the host-bound-specific checks, the policy rejects structurally unsafe values on construction regardless of mode — a <code>path</code> or <code>domain</code> containing a <code>;</code> (which would inject another <code>Set-Cookie</code> attribute) or control characters, and a non-boolean <code>host_bound</code> value:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">array_key_exists</span>(<span style="color:#e6db74">&#39;host_bound&#39;</span>, $options)) {
</span></span><span style="display:flex;"><span>    $parsed <span style="color:#f92672">=</span> <span style="color:#a6e22e">filter_var</span>(
</span></span><span style="display:flex;"><span>        $options[<span style="color:#e6db74">&#39;host_bound&#39;</span>],
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">FILTER_VALIDATE_BOOLEAN</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">FILTER_NULL_ON_FAILURE</span>,
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> ($parsed <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">InvalidSessionCookiePolicyException</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;session.cookie.host_bound must be a boolean (or a documented boolean string such as &#34;true&#34;/&#34;false&#34;).&#39;</span>,
</span></span><span style="display:flex;"><span>        );
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    $options[<span style="color:#e6db74">&#39;host_bound&#39;</span>] <span style="color:#f92672">=</span> $parsed;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Documented legacy string forms (<code>&quot;1&quot;</code>, <code>&quot;0&quot;</code>, <code>&quot;on&quot;</code>, <code>&quot;off&quot;</code>) still parse correctly — this rejects garbage, not backward compatibility.</p>
<h2 id="what-consumers-see">What Consumers See</h2>
<p>None of this changes how Inertia/Vue or vanilla <code>fetch</code> consumers read the CSRF cookie — that contract is unchanged:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-vue" data-lang="vue"><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span> <span style="color:#a6e22e">setup</span> <span style="color:#a6e22e">lang</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ts&#34;</span>&gt;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">useForm</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@inertiajs/vue3&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">form</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">useForm</span>({ <span style="color:#a6e22e">file</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">null</span> <span style="color:#a6e22e">as</span> <span style="color:#a6e22e">File</span> <span style="color:#f92672">|</span> <span style="color:#66d9ef">null</span> })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">submit</span>() {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">form</span>.<span style="color:#a6e22e">post</span>(<span style="color:#e6db74">&#39;/ingest/upload&#39;</span>, { <span style="color:#a6e22e">forceFormData</span><span style="color:#f92672">:</span> <span style="color:#66d9ef">true</span> })
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// Inertia&#39;s axios reads the cookie and forwards X-XSRF-TOKEN automatically.
</span></span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">script</span>&gt;
</span></span></code></pre></div><p>Inertia&rsquo;s adapter looks for a cookie literally named <code>XSRF-TOKEN</code> unless you tell it otherwise. That&rsquo;s why the Admin SPA&rsquo;s cookie readers were unified into one shared decoder (<code>packages/admin/app/utils/csrfCookie.ts</code>) that reads the <em>configured</em> <code>csrfCookieName</code> from runtime config, rather than each consumer hard-coding the default name. When host-bound mode renames the cookie to <code>__Host-XSRF-TOKEN</code>, every packaged Admin HTML response — the SPA fallback and the prebuilt <code>.html</code> assets — has that name rewritten into it from the runtime policy, so the served bundle and the actual cookie name never disagree.</p>
<h2 id="why-this-matters-beyond-waaseyaa">Why This Matters Beyond Waaseyaa</h2>
<p>The pattern generalizes past this one framework:</p>
<ul>
<li>If you support <code>__Host-</code>-prefixed cookies anywhere, <strong>validate the full contract programmatically</strong>, not just at code review. The failure mode is silent.</li>
<li>When two cookies (session + CSRF) share a security posture, <strong>govern them from one policy object</strong>, not two copies of the same logic that can drift.</li>
<li>Prefer <strong>rejecting bad config at boot</strong> over &ldquo;best-effort&rdquo; defaults that paper over a mistake. A crash on deploy is a bug report with a stack trace. A silently dropped cookie is a support ticket with no clues.</li>
</ul>
<p>Baamaapii</p>
]]></content:encoded></item></channel></rss>