From 7aa9bbd3bed7cc970f064e93734aba58d4df347b Mon Sep 17 00:00:00 2001 From: James Sumpter Date: Fri, 2 Oct 2026 19:56:42 -0700 Subject: [PATCH] fix(server): gate cost/log endpoints against cross-site requests and ship a full CSP The cost-bearing endpoints (/api/realtime/token, /api/openai/hud-summary, /api/google/nearby-places) and the voice debug log refuse cross-site browser requests: a foreign or opaque Origin, a Sec-Fetch-Site other than same-origin/none, or proxy forwarding headers. Loopback tools without an Origin and the LAN opt-in keep working. The dev and preview servers send one shared Content-Security-Policy, and embed-mode framing rewrites only the frame-ancestors directive of it. Salvaged from #242 onto current main. Co-authored-by: Sameh Khamis Co-Authored-By: Claude Opus 5.5 (1M context) --- SECURITY.md | 2 + build/embed-framing.js | 18 +++- build/vite.js | 48 +++++++-- scripts/format-scope.json | 1 + scripts/package-boundaries.json | 4 + server/providers/common/same-site.js | 43 ++++++++ server/providers/openai.js | 11 +- server/providers/places/google.js | 4 + src/localRequestGate.mjs | 147 +++++++++++++++++++++++++++ src/localRequestGate.test.mjs | 107 +++++++++++++++++++ src/tooling/viteBuild.test.mjs | 32 +++++- 11 files changed, 402 insertions(+), 15 deletions(-) create mode 100644 server/providers/common/same-site.js create mode 100644 src/localRequestGate.mjs create mode 100644 src/localRequestGate.test.mjs diff --git a/SECURITY.md b/SECURITY.md index e42e3113b..28b1201f3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -71,6 +71,8 @@ The data proxies under `server/providers/` are written so the browser cannot tur - **Sanitized errors** — internal error details are not echoed back to clients. - **Coalesced OAuth refresh** and cached successful responses only (OpenSky). - **Redacted debug logging.** The voice debug log (`.gev-logs/`, gitignored) strips API keys, bearer tokens, client secrets, and image data URLs before writing. +- **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`) and log (`/api/realtime/debug-log`) endpoints refuse cross-site browser requests: a foreign or opaque `Origin`, a `Sec-Fetch-Site` other than `same-origin`/`none` (this is what blocks an ``/navigation that carries no `Origin`), or any reverse-proxy/CDN forwarding header returns `403`. Non-browser loopback tools (the repo QA harnesses POST the token endpoint with no `Origin`) and the explicit `HOST=0.0.0.0` LAN opt-in keep working — the gate deliberately does not require a loopback remote address; that stricter requirement belongs only to the credential panel (`admitKeySetupRequest`). **Limit: this gate does not survive DNS rebinding in the `HOST=0.0.0.0` mode.** By default Vite's host check (`allowedHosts`) rejects a rebinding `Host` before any handler runs, and the gate is defense in depth behind it. The LAN opt-in turns that host check off, so an attacker-controlled name that resolves to the operator's address yields a page whose `Origin` and `Host` agree and whose `Sec-Fetch-Site` is a truthful `same-origin`; all three checks pass. In that mode protection rests on the operator's network, not on `Origin`/`Host` agreement (see Network exposure below). +- **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:`: no inline script and no script from any other origin; `blob:` covers the object URLs Cesium's bundled workers bootstrap through in the built app), alongside the existing `X-Frame-Options: DENY` and `frame-ancestors 'none'`. `'unsafe-eval'` stays because Knockout, bundled inside Cesium's widgets package, resolves the global object with `eval` at load time and the globe does not initialize without it; styles permit `'unsafe-inline'` and Google Fonts because Vite's dev client injects `