From e6f1dd70a195c35b49e30ea56a38dd3bb2595314 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 2 Oct 2026 20:15:54 -0700 Subject: [PATCH] fix(server): apply the Host check before the app's own routes Vite installs its Host check after the middleware plugins add in configureServer, so the provider and /api routes answered any Host, including a DNS-rebinding name. hostCheckPlugin runs first on the dev and preview servers and applies Vite's rule with the same allowed hosts: IP addresses, localhost and *.localhost, the configured server host, and the listed names. A test drives a real Vite server with an unlisted Host. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++ SECURITY.md | 2 +- build/allowedHosts.js | 68 +++++++++++++++++ build/vite.js | 4 +- src/tooling/allowedHosts.test.mjs | 119 ++++++++++++++++++++++++++++++ src/tooling/viteBuild.test.mjs | 9 ++- 6 files changed, 203 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 469e4d3ac..9f6dfd2a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +- Apply the Host check before the app's own routes. Vite installs its Host + check after the middleware that plugins add, so the provider and `/api` + routes used to answer any Host, including a DNS-rebinding name, in every + mode. A first-running middleware now applies the same allowed hosts on the + dev and preview servers. + - Refuse cross-site browser requests to the cost-bearing and log endpoints (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/realtime/debug-log`): a foreign or opaque diff --git a/SECURITY.md b/SECURITY.md index 5d5040564..9a35a73e4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -71,7 +71,7 @@ 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`). Vite's host check (`allowedHosts`) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control. +- **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`). The Host check (`allowedHosts`, applied by `hostCheckPlugin` ahead of every route, since Vite's own check runs only after plugin middleware) rejects a rebinding `Host` before any handler runs, in every mode, and the gate is defense in depth behind it: the LAN opt-in keeps that check and accepts only IP addresses, `localhost` and names listed in `GEV_ALLOWED_HOSTS`. **Limit:** a name you list there is trusted like the app's own address, so list only names you control. - **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 `