tenant-iso-cookies
What changes
Platform mitigation for #217 (oncarlos.com is not on the Public Suffix List). On a response proxied from a tenant route whose request host is under the apps domain (PLATFORMAPPSDOMAIN), the edge removes the Domain attribute from any Set-Cookie whose Domain is neither the request host nor within the account's own <sqid>.<apps domain>. The cookie becomes host-only; the rest of the line (name, value, other attributes, their order and spelling) is unchanged.
Domain=oncarlos.com,Domain=<other-sqid>.oncarlos.com, or an unrelated domain: rewritten.Domain=<host>andDomain=<own-sqid>.oncarlos.com(or under it): passed, so an account's apps can still share a cookie.- Custom domains, and every host when PLATFORMAPPSDOMAIN is unset: untouched.
Also: the response cache no longer stores a response that carries Set-Cookie as a trailer. An undeclared trailer reached the recorder under the raw key Trailer:Set-Cookie, which the header Set-Cookie refusal never saw. The response was cached, and writeCached replayed one visitor's cookie to every later visitor as a trailer. maybeStore now refuses it exactly as it refuses a header Set-Cookie (hasTrailerSetCookie; the key is matched by hand because it is not canonical).
Design
- Where: the tenant proxies'
ModifyResponsehook.purgeModifieris now wrapped byresponseModifier(purge, then cookies) at the four proxy sites inReload. It edits the upstream response's own header map before httputil copies it, so there is no new ResponseWriter wrapper and no shared header map (the AGENTS.md rule). The cache recorder, the gzip layer and the 101 upgrade path only ever see the rewritten form. - Account namespace: the sqid comes from the request host's own labels (
release.HostAccountLabel), and only when the serving route'sAccountagrees with it. A row that names another account, or none, gets host-only scope. This is strict on purpose: getting it wrong only narrows a cookie. - Parsing (RFC 6265 §5.2): attribute names are matched case-insensitively, with surrounding whitespace ignored; a leading dot and a trailing dot are stripped before comparing. An empty Domain is left alone, because user agents ignore it and the cookie is already host-only. Every
Set-Cookieline is checked. If a line has several Domain attributes and any of them is out of scope, all of them are removed. - Trailers: the body is wrapped (every status except 101), and on Close, which httputil calls before it copies trailers,
resp.Traileris rewritten. - Logging: the first rewrite per route per process is logged, naming the route host, app and account (a
sync.Mapkeyed by route host, so it is bounded by the routing table). - Platform side: neither of the platform's
http.Cookies (console session, gate) sets a Domain. The gate cookie is set by the edge itself, not proxied. Static and status routes have no hook, and their headers never carry tenant cookies.
Tests (internal/edge/cookiescope_test.go)
- The apps domain, another account's namespace, an unrelated domain, mixed-case and leading-dot spellings, a trailing dot, and multiple Domain attributes are all rewritten. Seven cookies on one response.
- The account's own namespace, the host, a sibling host in the account, host-only cookies, an empty Domain, and a cookie named
domainall pass byte-for-byte. - The namespace is refused when the route's Account is another account or empty.
- Custom domain: unchanged. No apps domain configured: unchanged.
- Cache path: a cacheable response delivers rewritten cookies and is still not stored.
- Trailer cookie: two visitors in turn each get their own rewritten trailer cookie fresh from the app, and nothing is cached.
maybeStorerefusesTrailer:Set-Cookiein either spelling. Both tests fail without the refusal. - One log line per route across repeated responses.
- Unit table for
scopeSetCookie. - Without the wiring, 5 of the original 8 integration tests fail.
Gates: gofmt clean, go build ./..., go vet ./..., edge and docsite packages pass. The only failures are the known environmental ones: the two TestListenPurgeClose* tests (socket-path length) and, in make test, the root package (~/.carlos/config) plus the socket-path-length tests in activator, host and instanceexec.
Deliberately not done
- Same-site CSRF between accounts stays open until the PSL entry lands (#217).
Set-Cookieon 1xx (103 Early Hints) is forwarded by httputil without ModifyResponse, so it is not rewritten. Browsers do not apply cookies from 1xx responses.- Docs: one paragraph under
carlos domainsin docs/site/cli.md.
Discussions 1 open
Tasks
- No tasks yet.
Activity
- claude agent, operated by paul@keymail.dev replied to oncarlos.com is not on the Public Suffix List: tenants share cookies and count as same-site
- claude agent, operated by paul@keymail.dev replied to oncarlos.com is not on the Public Suffix List: tenants share cookies and count as same-site
- claude agent, operated by paul@keymail.dev replied to oncarlos.com is not on the Public Suffix List: tenants share cookies and count as same-site
- claude agent, operated by paul@keymail.dev opened oncarlos.com is not on the Public Suffix List: tenants share cookies and count as same-site