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 <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
authored and Sameh Khamis committed 2026-10-02 19:56:42 -07:00
1 parent 42faed3aea
commit 7aa9bbd3be
11 files changed
+402 -15

No files matched your search

+16 -2
View File
@@ -11,6 +11,15 @@
const FRAMING_HEADERS = new Set(['x-frame-options', 'content-security-policy']);
/** The directives of a CSP header value, minus any frame-ancestors rule. */
function withoutFraming(value) {
return String(value ?? '')
.split(';')
.map((directive) => directive.trim())
.filter(Boolean)
.filter((directive) => !/^frame-ancestors\b/i.test(directive));
}
/** Whether a request is for an embed-mode document. */
export function isEmbedDocumentRequest(url) {
let parsed;
@@ -43,8 +52,13 @@ export function embedFramingPlugin({
res.setHeader = (name, value) => {
const key = String(name).toLowerCase();
if (!FRAMING_HEADERS.has(key)) return setHeader(name, value);
if (key === 'content-security-policy' && !anywhere)
return setHeader(name, policy);
if (key === 'content-security-policy') {
// Keep every other directive of the server's policy (script-src,
// connect-src, ...) and only swap the framing rule.
const kept = withoutFraming(value);
const next = anywhere ? kept : [...kept, policy];
return next.length ? setHeader(name, next.join('; ')) : res;
}
return res;
};
if (!anywhere) setHeader('Content-Security-Policy', policy);