Summary
handleAnalyticsProxy (the first-party reverse proxy for the self-hosted Umami
tracker, added in #595) strips the response set-cookie header but does
not strip the request cookie header. So the browser's first-party
cookies for the proxy origin (gittensory.aethereal.dev) — including any
session / auth / OAuth-state cookie whose path matches /stats/* — are forwarded
verbatim to the Umami upstream (tasty.aethereal.dev) on every
GET /stats/script.js and POST /stats/api/send.
This directly contradicts the design as documented in the same change.
The stated invariant
The #595 commit message:
The proxy is a strict path allowlist … strips cookies/hop-by-hop headers,
forwards the real client IP for geolocation, and fails closed (502)…
and the file header comment ("cookieless analytics"). The response side honors
it; the request side does not.
Evidence
// apps/gittensory-ui/src/lib/analytics-proxy.ts:26
const STRIP_REQUEST_HEADERS = new Set([
"host", "connection", "keep-alive", "transfer-encoding", "upgrade",
"cf-connecting-ip", "cf-ipcountry", "cf-ray", "cf-visitor",
"x-forwarded-host", "x-forwarded-proto",
]); // <-- no "cookie"
// :43 (response side, correctly stripped)
const STRIP_RESPONSE_HEADERS = new Set([
"connection", "keep-alive", "transfer-encoding", "content-encoding",
"content-length",
"set-cookie", // cookieless analytics: never relay cookies to the client
]);
// :71 forwards every request header except the strip-list -> cookie is forwarded
const headers = new Headers();
request.headers.forEach((value, key) => {
if (!STRIP_REQUEST_HEADERS.has(key.toLowerCase())) headers.set(key, value);
});
Reachability
Live in production. apps/gittensory-ui/src/server.ts runs the proxy ahead of
SSR for every request:
const analytics = await handleAnalyticsProxy(request);
if (analytics) return analytics;
When a logged-in visitor loads any page, the browser fetches
/stats/script.js and the tracker POSTs /stats/api/send — both same-origin
requests that the browser decorates with the origin's cookies. Those cookies are
then forwarded to tasty.aethereal.dev.
gittensory.aethereal.dev does set first-party cookies (e.g. the GitHub OAuth
state cookie via buildGitHubOAuthStateCookie, and the authenticated session),
so this is not hypothetical.
Impact
- Privacy guarantee broken. Umami is deployed precisely for "cookieless, no
PII" analytics; forwarding the visitor's cookies to the analytics host
undermines that and lands those cookies in the analytics server's request
logs.
- Expanded credential exposure. Session / auth cookies now traverse to, and
may be logged by, a second host. A compromise or log leak of the analytics
host would expose live gittensory session cookies — a surface that should
not exist for an analytics beacon.
Test status
Not locked in — there is no test for handleAnalyticsProxy (no analytics-proxy
test file). No test asserts the request header strip-list.
Suggested fix
Strip cookie from forwarded requests, mirroring the response-side set-cookie
strip:
const STRIP_REQUEST_HEADERS = new Set([
"host", "connection", "keep-alive", "transfer-encoding", "upgrade",
"cookie", // cookieless analytics: never forward first-party cookies upstream
"cf-connecting-ip", "cf-ipcountry", "cf-ray", "cf-visitor",
"x-forwarded-host", "x-forwarded-proto",
]);
Add a unit test for handleAnalyticsProxy asserting that a request with a
Cookie header reaches the upstream fetch with no cookie header (and,
for parity, that set-cookie is absent from the returned response).
Secondary note (same function, lower severity)
x-forwarded-for is not in the strip-list, and the proxy appends the real
cf-connecting-ip after any client-supplied x-forwarded-for
(existing ? ${existing}, ${clientIp}`` at :78–79). A client can therefore
prepend a spoofed IP that Umami may treat as the visitor's address for
geolocation. Setting x-forwarded-for to the trusted `cf-connecting-ip` only
(overwrite, not append) closes this and is consistent with the cookie fix.
Distinct from prior reports
Not a duplicate of #461 (that restricted the public GitHub stats API proxy
to known repos). This is the UI Umami tracker reverse-proxy and a
request-header forwarding/strip defect, in newly-added #595 code.
Summary
handleAnalyticsProxy(the first-party reverse proxy for the self-hosted Umamitracker, added in #595) strips the response
set-cookieheader but doesnot strip the request
cookieheader. So the browser's first-partycookies for the proxy origin (
gittensory.aethereal.dev) — including anysession / auth / OAuth-state cookie whose path matches
/stats/*— are forwardedverbatim to the Umami upstream (
tasty.aethereal.dev) on everyGET /stats/script.jsandPOST /stats/api/send.This directly contradicts the design as documented in the same change.
The stated invariant
The #595 commit message:
and the file header comment ("cookieless analytics"). The response side honors
it; the request side does not.
Evidence
Reachability
Live in production.
apps/gittensory-ui/src/server.tsruns the proxy ahead ofSSR for every request:
When a logged-in visitor loads any page, the browser fetches
/stats/script.jsand the trackerPOSTs/stats/api/send— both same-originrequests that the browser decorates with the origin's cookies. Those cookies are
then forwarded to
tasty.aethereal.dev.gittensory.aethereal.devdoes set first-party cookies (e.g. the GitHub OAuthstate cookie via
buildGitHubOAuthStateCookie, and the authenticated session),so this is not hypothetical.
Impact
PII" analytics; forwarding the visitor's cookies to the analytics host
undermines that and lands those cookies in the analytics server's request
logs.
may be logged by, a second host. A compromise or log leak of the analytics
host would expose live
gittensorysession cookies — a surface that shouldnot exist for an analytics beacon.
Test status
Not locked in — there is no test for
handleAnalyticsProxy(noanalytics-proxytest file). No test asserts the request header strip-list.
Suggested fix
Strip
cookiefrom forwarded requests, mirroring the response-sideset-cookiestrip:
Add a unit test for
handleAnalyticsProxyasserting that a request with aCookieheader reaches the upstreamfetchwith nocookieheader (and,for parity, that
set-cookieis absent from the returned response).Secondary note (same function, lower severity)
x-forwarded-foris not in the strip-list, and the proxy appends the realcf-connecting-ipafter any client-suppliedx-forwarded-for(
existing ?${existing}, ${clientIp}`` at :78–79). A client can thereforeprepend a spoofed IP that Umami may treat as the visitor's address for
geolocation. Setting
x-forwarded-forto the trusted `cf-connecting-ip` only(overwrite, not append) closes this and is consistent with the cookie fix.
Distinct from prior reports
Not a duplicate of #461 (that restricted the public GitHub stats API proxy
to known repos). This is the UI Umami tracker reverse-proxy and a
request-header forwarding/strip defect, in newly-added #595 code.