carlos / platform Public

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> and Domain=<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' ModifyResponse hook. purgeModifier is now wrapped by responseModifier (purge, then cookies) at the four proxy sites in Reload. 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's Account agrees 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-Cookie line 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.Trailer is rewritten.
  • Logging: the first rewrite per route per process is logged, naming the route host, app and account (a sync.Map keyed 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 domain all 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. maybeStore refuses Trailer:Set-Cookie in 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-Cookie on 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 domains in docs/site/cli.md.
Commits 0
Discussions 1 1 open
Tasks 0/0 done

Activity