Make every cookie host-only: __Host- names, Secure, Path=/ #11

Merged
heimdall merged 2 commits from heimdall/host-only-cookies into main 2026-11-09 11:48:28 +00:00
Member

Makes every cookie Bifrost sets host-only: __Host- name, Secure, Path=/, no Domain. HttpOnly and SameSite are unchanged.

Reason. The preview host is same-site with the production host. A sibling subdomain of gamecult.org can set a Domain=gamecult.org cookie with the same name and shadow ours (cookie tossing / session fixation). Browsers refuse a __Host- cookie that arrives with a Domain, so the planted cookie never replaces ours.

Cookie Was Now
auth session .AspNetCore.Cookies __Host-bifrost_session, Secure always, Path=/
OAuth correlation .AspNetCore.Correlation.* __Host-bifrost_oauth_correlation.*, Secure, Path=/
antiforgery .AspNetCore.Antiforgery.* __Host-bifrost_antiforgery (not in the brief; it is a cookie Razor Pages sets, so it follows the same rule)
voter bifrost_voter, Path=/mill __Host-bifrost_voter, Path=/
Heimdall attempt ring bifrost_heimdall_0/1, Path=/auth/heimdall __Host-bifrost_heimdall_0/1, Path=/

Names live in one place, Features/Shared/BifrostCookies.cs; every reader, writer and test uses it.

Cost.

  • One-time sign-out for every member on deploy (the session cookie is renamed), and in-flight OAuth or Heimdall attempts fail once. Voters get a fresh device id on their next cast, so a prior anonymous device id no longer counts as theirs.
  • Wider path: __Host- forbids a narrower Path, so the voter and attempt cookies are now sent on every request to the host, not only /mill and /auth/heimdall. Both are HttpOnly, Secure and protected values; the attempt ring is still capped at two cookies (~2.4 KB worst case) so the Cookie header stays inside nginx's limit.

Tests. New CookieHardeningTests pins the literal names, Secure, Path=/ and no Domain for each cookie, and checks real Set-Cookie headers for the voter and attempt cookies (including deletion). I removed each guard in turn and the tests failed, with one exception: CorrelationCookie.SecurePolicy is already Always by framework default, so that explicit line pins behaviour rather than changing it. Existing voter and Heimdall tests now read the shared constants and expect path=/.

Suite on this host (a .NET 8 target run on the only installed runtime, .NET 10, via roll-forward): 508 pass, 38 fail. The same 38 fail on unmodified main (BridgeCli, ConceptTallyExport, OidcIssuer, ProviderAdvertisement, one ConceptVoteRateLimit); none touch cookies. Worth a clean CI run on the proper runtime before merge.

Deploy. Not deployed. It waits on the Bifrost lag.

🤖 Generated with Claude Code

Makes every cookie Bifrost sets host-only: `__Host-` name, `Secure`, `Path=/`, no `Domain`. `HttpOnly` and `SameSite` are unchanged. **Reason.** The preview host is same-site with the production host. A sibling subdomain of gamecult.org can set a `Domain=gamecult.org` cookie with the same name and shadow ours (cookie tossing / session fixation). Browsers refuse a `__Host-` cookie that arrives with a Domain, so the planted cookie never replaces ours. | Cookie | Was | Now | |---|---|---| | auth session | `.AspNetCore.Cookies` | `__Host-bifrost_session`, Secure always, Path=/ | | OAuth correlation | `.AspNetCore.Correlation.*` | `__Host-bifrost_oauth_correlation.*`, Secure, Path=/ | | antiforgery | `.AspNetCore.Antiforgery.*` | `__Host-bifrost_antiforgery` (not in the brief; it is a cookie Razor Pages sets, so it follows the same rule) | | voter | `bifrost_voter`, Path=/mill | `__Host-bifrost_voter`, Path=/ | | Heimdall attempt ring | `bifrost_heimdall_0/1`, Path=/auth/heimdall | `__Host-bifrost_heimdall_0/1`, Path=/ | Names live in one place, `Features/Shared/BifrostCookies.cs`; every reader, writer and test uses it. **Cost.** - One-time sign-out for every member on deploy (the session cookie is renamed), and in-flight OAuth or Heimdall attempts fail once. Voters get a fresh device id on their next cast, so a prior anonymous device id no longer counts as theirs. - Wider path: `__Host-` forbids a narrower Path, so the voter and attempt cookies are now sent on every request to the host, not only `/mill` and `/auth/heimdall`. Both are HttpOnly, Secure and protected values; the attempt ring is still capped at two cookies (~2.4 KB worst case) so the Cookie header stays inside nginx's limit. **Tests.** New `CookieHardeningTests` pins the literal names, `Secure`, `Path=/` and no Domain for each cookie, and checks real `Set-Cookie` headers for the voter and attempt cookies (including deletion). I removed each guard in turn and the tests failed, with one exception: `CorrelationCookie.SecurePolicy` is already `Always` by framework default, so that explicit line pins behaviour rather than changing it. Existing voter and Heimdall tests now read the shared constants and expect `path=/`. Suite on this host (a .NET 8 target run on the only installed runtime, .NET 10, via roll-forward): 508 pass, 38 fail. The same 38 fail on unmodified main (BridgeCli, ConceptTallyExport, OidcIssuer, ProviderAdvertisement, one ConceptVoteRateLimit); none touch cookies. Worth a clean CI run on the proper runtime before merge. **Deploy.** Not deployed. It waits on the Bifrost lag. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Session, OAuth correlation, antiforgery, voter and Heimdall attempt cookies all take the
__Host- prefix. The prefix forbids a narrower Path, so the voter (was /mill) and attempt
(was /auth/heimdall) cookies move to Path=/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A __Host- cookie is only removed by a Set-Cookie that is itself Secure. The
hardening test now checks Secure on every header, deletion included, and the
completion tests pin that success and refusal both clear the attempt cookie
with Secure, Path=/ and no Domain.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Bifrost!11
No description provided.