mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(server): let the CSP load the Nepal event's embedded media
The Content-Security-Policy blocked the YouTube player API, the Facebook video SDK and X's post widget that src/data/bhoteKoshiEmbeddedMedia.js loads, along with their players and posts in frames. script-src now allows exactly those three script origins and a new frame-src allows YouTube, youtube-nocookie, Facebook and X embeds. A test ties the policy to the frames the media module builds and pins the allowed script origins. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
913d45bc07
commit
0646630dec
3 files changed
+54
-3
No files matched your search
+1
-1
@@ -72,7 +72,7 @@ The data proxies under `server/providers/` are written so the browser cannot tur
|
|||||||
- **Coalesced OAuth refresh** and cached successful responses only (OpenSky).
|
- **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.
|
- **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`, `/api/google/text-search`) 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 `<img>`/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.
|
- **Cross-site gate on cost/log endpoints.** The cost-bearing (`/api/realtime/token`, `/api/openai/hud-summary`, `/api/google/nearby-places`, `/api/google/text-search`) 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 `<img>`/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 `<style>` elements.
|
- **Full Content-Security-Policy.** The dev and preview servers send a real CSP (`script-src 'self' 'unsafe-eval' blob:` plus the YouTube, Facebook and X scripts the Nepal event's embedded media loads, with a matching `frame-src` for their players and posts: no inline script and no other script 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 `<style>` elements.
|
||||||
|
|
||||||
## Network exposure — the operator threat model
|
## Network exposure — the operator threat model
|
||||||
|
|
||||||
|
|||||||
+8
-2
@@ -6,7 +6,9 @@ import { panelBuildPlugin } from './panel.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Content-Security-Policy for every document the dev/preview server serves.
|
* Content-Security-Policy for every document the dev/preview server serves.
|
||||||
* No inline script and no foreign script origin is permitted. 'unsafe-eval' is
|
* No inline script is permitted, and the only foreign script origins are the
|
||||||
|
* three that the Bhote Koshi event's embedded media needs (YouTube, Facebook
|
||||||
|
* and X), which frame-src matches for their players and posts. 'unsafe-eval' is
|
||||||
* required: Knockout (bundled inside @cesium/widgets) resolves the global
|
* required: Knockout (bundled inside @cesium/widgets) resolves the global
|
||||||
* object with `(0, eval)("this")` at module load, and without it the Cesium
|
* object with `(0, eval)("this")` at module load, and without it the Cesium
|
||||||
* widget never initializes (verified in headless Chrome). It also covers
|
* widget never initializes (verified in headless Chrome). It also covers
|
||||||
@@ -17,7 +19,9 @@ import { panelBuildPlugin } from './panel.js';
|
|||||||
*/
|
*/
|
||||||
export const BROWSER_CSP = [
|
export const BROWSER_CSP = [
|
||||||
"default-src 'self'",
|
"default-src 'self'",
|
||||||
"script-src 'self' 'unsafe-eval' blob:",
|
// The Bhote Koshi event's embedded media: the YouTube player API, the
|
||||||
|
// Facebook video SDK and X's post widget (src/data/bhoteKoshiEmbeddedMedia.js).
|
||||||
|
"script-src 'self' 'unsafe-eval' blob: https://www.youtube.com https://connect.facebook.net https://platform.twitter.com",
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
||||||
"font-src 'self' data: https://fonts.gstatic.com",
|
"font-src 'self' data: https://fonts.gstatic.com",
|
||||||
"img-src 'self' data: blob: https:",
|
"img-src 'self' data: blob: https:",
|
||||||
@@ -25,6 +29,8 @@ export const BROWSER_CSP = [
|
|||||||
"connect-src 'self' blob: data: https: wss: ws:",
|
"connect-src 'self' blob: data: https: wss: ws:",
|
||||||
"worker-src 'self' blob:",
|
"worker-src 'self' blob:",
|
||||||
"child-src 'self' blob:",
|
"child-src 'self' blob:",
|
||||||
|
// The same media's players and posts, which load in frames.
|
||||||
|
"frame-src 'self' blob: https://www.youtube-nocookie.com https://www.youtube.com https://www.facebook.com https://platform.twitter.com",
|
||||||
"manifest-src 'self'",
|
"manifest-src 'self'",
|
||||||
"object-src 'none'",
|
"object-src 'none'",
|
||||||
"base-uri 'self'",
|
"base-uri 'self'",
|
||||||
|
|||||||
@@ -175,3 +175,48 @@ test('only embed-mode documents may be framed, and only by the allowed ancestors
|
|||||||
assert.equal(plugin.configurePreviewServer, undefined);
|
assert.equal(plugin.configurePreviewServer, undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the CSP admits the event media embeds, and no other script origin', async () => {
|
||||||
|
const { embeddedMediaFrameUrl, resolveEmbeddedMediaSource } =
|
||||||
|
await import('../../src/data/bhoteKoshiEmbeddedMedia.js');
|
||||||
|
const directive = (name) =>
|
||||||
|
BROWSER_CSP.split(';')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.find((part) => part.startsWith(`${name} `))
|
||||||
|
?.split(/\s+/)
|
||||||
|
.slice(1) ?? [];
|
||||||
|
const scripts = directive('script-src');
|
||||||
|
const frames = directive('frame-src');
|
||||||
|
// The player API, the Facebook SDK and X's widget script.
|
||||||
|
for (const origin of [
|
||||||
|
'https://www.youtube.com',
|
||||||
|
'https://connect.facebook.net',
|
||||||
|
'https://platform.twitter.com',
|
||||||
|
])
|
||||||
|
assert.ok(scripts.includes(origin), origin);
|
||||||
|
// Only those three, and never a wildcard or a scheme.
|
||||||
|
assert.deepEqual(
|
||||||
|
scripts.filter(
|
||||||
|
(source) => source.startsWith('http') || source === 'https:',
|
||||||
|
),
|
||||||
|
[
|
||||||
|
'https://www.youtube.com',
|
||||||
|
'https://connect.facebook.net',
|
||||||
|
'https://platform.twitter.com',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
assert.ok(!scripts.some((source) => source.includes('*')));
|
||||||
|
// Every frame the event builds for a YouTube or Facebook source is allowed.
|
||||||
|
for (const link of [
|
||||||
|
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||||
|
'https://youtu.be/dQw4w9WgXcQ',
|
||||||
|
'https://www.facebook.com/facebook/videos/10153231379946729/',
|
||||||
|
]) {
|
||||||
|
const source = resolveEmbeddedMediaSource(link);
|
||||||
|
assert.ok(source, link);
|
||||||
|
const origin = new URL(embeddedMediaFrameUrl(source, {})).origin;
|
||||||
|
assert.ok(frames.includes(origin), `${link} -> ${origin}`);
|
||||||
|
}
|
||||||
|
// X posts load in X's own frames.
|
||||||
|
assert.ok(frames.includes('https://platform.twitter.com'));
|
||||||
|
});
|
||||||
Reference in new issue
Block a user