refactor(server): share one proxy-header check across the local gates

localRequestGate.mjs owns the reverse-proxy header list and hasProxySignals;
Provider Settings, the cross-site gate and /mcp all use it instead of two
copies of the same nine names. The changelog credits the cross-site gate.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Sameh KhamisandClaude Opus 5.5 committed 2026-10-02 20:02:48 -07:00
1 parent bbf4976f5e
commit 6cdd6c8af9
5 files changed
+34 -34

No files matched your search

+7
View File
@@ -2,6 +2,13 @@
## [Unreleased]
- 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
Origin, a cross-site `Sec-Fetch-Site`, or proxy forwarding headers get 403,
while loopback tools and LAN use keep working. The dev and preview servers
send one shared Content-Security-Policy (James Sumpter, #242).
- The voice debug log records the server's own time: a record can no longer
supply its `loggedAt`. The dev and preview servers also send
`X-Content-Type-Options: nosniff` (findings by Sunil, #710).
+7 -1
View File
@@ -306,6 +306,7 @@
"server/providers/common/source-root.js",
"server/standalone/key-setup.js",
"src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"server/standalone/key-setup-hardening.mjs"
],
"external": []
@@ -390,7 +391,8 @@
"src/ui/mapSource.js",
"src/ui/mapSourceControls.js",
"src/mapStackChips.js",
"src/keySetupCore.mjs"
"src/keySetupCore.mjs",
"src/localRequestGate.mjs"
],
"external": []
},
@@ -409,6 +411,7 @@
"src/loadingFeedback.js",
"src/data/installationFeedback.js",
"src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"src/data/feedState.js",
"src/data/layerSnapshot.js"
],
@@ -529,6 +532,7 @@
"src/data/militaryAwarenessEngine.js",
"src/data/scenePick.js",
"src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"src/loadingFeedback.js",
"src/locationStatus.js",
"src/mapStackChips.js",
@@ -1266,6 +1270,7 @@
"src/maps/terrain.js",
"src/maps/terrainRetry.js",
"src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"src/maps/imageryHost.js"
],
"external": ["cesium"]
@@ -1511,6 +1516,7 @@
"src/hudSummaryResponse.js",
"src/keySetup.js",
"src/keySetupCore.mjs",
"src/localRequestGate.mjs",
"src/keylessGeocoder.js",
"src/layers/aircraft/classification.js",
"src/layers/alpr/index.js",
+2 -1
View File
@@ -7,7 +7,8 @@
* rebinding) and, when a browser sends an Origin, come from that same host.
*/
import { hasProxySignals, isSharingEnabled } from '../../src/keySetupCore.mjs';
import { isSharingEnabled } from '../../src/keySetupCore.mjs';
import { hasProxySignals } from '../../src/localRequestGate.mjs';
import { createMcpHttpHandler } from '../../src/tools/mcp/index.js';
import { createLocalMcpServer } from './server.js';
+4 -25
View File
@@ -1,7 +1,8 @@
/**
* Key setup ("POWER UP") — the pure core.
*
* One registry, three pure functions, zero dependencies. The dev server's
* One registry and pure functions; its only import is the proxy-header check
* shared with the other local gates (localRequestGate.mjs). The dev server's
* /api/setup endpoints (vite.config.js) and the in-app panel (keySetup.js)
* are both thin shells over this module, so what a key is called, what it
* unlocks, and how a .env line is written each live in exactly one place.
@@ -11,6 +12,8 @@
* behavior below unit-testable.
*/
import { hasProxySignals } from './localRequestGate.mjs';
/** Longest accepted key/token value. Real provider keys are all far shorter. */
export const KEY_SETUP_VALUE_LIMIT = 512;
@@ -184,30 +187,6 @@ export function parseWindowsUserSid(stdout) {
: null;
}
/** Headers that reverse proxies, tunnels and CDNs add to a request. */
export const PROXY_SIGNAL_HEADERS = Object.freeze([
'forwarded',
'via',
'x-forwarded-for',
'x-forwarded-host',
'x-forwarded-port',
'x-forwarded-proto',
'x-real-ip',
'cf-connecting-ip',
'cf-ray',
]);
/**
* Whether a request carries reverse-proxy or CDN forwarding headers, so it
* did not originate on this machine whatever its socket says. `headers` is
* keyed by lower-case header name.
*/
export function hasProxySignals(headers = {}) {
return PROXY_SIGNAL_HEADERS.some(
(name) => String(headers[name] || '').trim() !== '',
);
}
/**
* Whether the launcher has sharing on. Every sharing signal the launcher
* recognizes (scripts/pinokio-preflight.mjs) counts, so the two sets cannot
+14 -7
View File
@@ -21,10 +21,9 @@
*/
/**
* Reverse-proxy / CDN forwarding headers. Their presence means the request did
* not originate on this machine, whatever its socket says. Kept as this
* module's own list (keySetupCore.mjs carries the same nine names for the
* credential panel) so the gate has no imports and can sit in any package
* boundary.
* not originate on this machine, whatever its socket says. This module owns
* the list for every local gate (the credential panel and /mcp import
* `hasProxySignals`), and has no imports so it can sit in any package boundary.
*/
export const PROXY_SIGNALS = Object.freeze([
'forwarded',
@@ -38,6 +37,16 @@ export const PROXY_SIGNALS = Object.freeze([
'cf-ray',
]);
/**
* Whether a request carries reverse-proxy or CDN forwarding headers. `headers`
* is keyed by lower-case header name.
*/
export function hasProxySignals(headers = {}) {
return PROXY_SIGNALS.some(
(name) => String(headers[name] || '').trim() !== '',
);
}
/**
* Compute a request's own authority (an origin string) from its protocol and
* Host header. Unlike the credential-panel gate's localhost-restricted
@@ -90,9 +99,7 @@ export function admitSameSiteRequest({
} = {}) {
// (1) A request carrying reverse-proxy / CDN forwarding headers did not
// originate on this machine, whatever its socket says.
if (
PROXY_SIGNALS.some((name) => String(proxyHeaders[name] || '').trim() !== '')
) {
if (hasProxySignals(proxyHeaders)) {
return {
ok: false,
status: 403,