mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44: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).
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
|
||||
+8
-2
@@ -6,7 +6,9 @@ import { panelBuildPlugin } from './panel.js';
|
||||
|
||||
/**
|
||||
* 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
|
||||
* object with `(0, eval)("this")` at module load, and without it the Cesium
|
||||
* widget never initializes (verified in headless Chrome). It also covers
|
||||
@@ -17,7 +19,9 @@ import { panelBuildPlugin } from './panel.js';
|
||||
*/
|
||||
export const BROWSER_CSP = [
|
||||
"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",
|
||||
"font-src 'self' data: https://fonts.gstatic.com",
|
||||
"img-src 'self' data: blob: https:",
|
||||
@@ -25,6 +29,8 @@ export const BROWSER_CSP = [
|
||||
"connect-src 'self' blob: data: https: wss: ws:",
|
||||
"worker-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'",
|
||||
"object-src 'none'",
|
||||
"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);
|
||||
}
|
||||
});
|
||||
|
||||
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