Decision record · DR-006
Limit where the page can send a visitor's key with a Content-Security-Policy
- Status
- Accepted
- Date
- 2026-10
- Applies to
- web/next.config.ts, web/src/lib/security/csp.ts
Decision in one line
Every page is served with a Content-Security-Policy that lets it connect only to this site, api.anthropic.com and api.openai.com, load images only from this site, never be framed, and post forms only to itself; inline scripts stay allowed because the pages are prerendered without per-request nonces.
Amends DR-004.
Context
DR-004 keeps the visitor's API key in sessionStorage, or localStorage if they ask, and calls the provider from the browser. Any script running on the page can read the key. The site loads no third-party scripts, but nothing at the HTTP layer limited where a script injected into the page (through a bug, or a compromised dependency) could send what it read. A review of the upgrade pointed this out.
Decision
- Send these headers on every route from
next.config.ts:connect-src 'self' https://api.anthropic.com https://api.openai.com, built from the provider endpoints inweb/src/lib/ai/models.ts, so adding a provider means adding it to the policy in the same place.default-src 'self',img-src 'self' data: blob:,font-src 'self',worker-src 'self' blob:,object-src 'none',base-uri 'self',form-action 'self'andframe-ancestors 'none'.script-src 'self' 'unsafe-inline'andstyle-src 'self' 'unsafe-inline', with'unsafe-eval'added in development only.X-Content-Type-Options: nosniffandReferrer-Policy: strict-origin-when-cross-origin.
- A unit test checks that
connect-srclists exactly this site and the provider origins.
Options considered
- No policy. Simplest, and the status quo, but no limit on where a key could go.
- A strict policy with nonces. Blocks injected inline scripts too, but a nonce must be new on every request, so every page would have to be rendered per request instead of served as a static file.
- Hash-based policy with Subresource Integrity. Keeps pages static, but is experimental in Next.js 16 and still needs allowances for the inline scripts the framework writes.
- A static policy with a tight
connect-src(chosen).
Why
The risk that matters for a BYOK site is the key leaving for somewhere other than the
provider. connect-src is the directive that governs fetch, XHR, WebSockets and beacons,
and img-src closes the old trick of loading an image from an attacker's server with the
key in the URL. Both work on a fully static site, which keeps hosting free and pages fast.
What happened
- The policy does not stop an injected inline script from running, because
'unsafe-inline'is allowed. It limits where that script can send data withfetchor an image, not what it can read. - It cannot stop a script from navigating the page to another site with the key in the address; no widely supported directive controls navigation.
- Browser extensions are not bound by a page's policy, so the advice to use a key with a spending limit stands.
- In the browser, the site worked unchanged under the policy (the theme script, the solver's
Web Worker and the provider calls), and a
fetchto another origin from the page was refused.
What I'd change
- If the site ever gets a server, move to nonces and drop
'unsafe-inline'for scripts. - Add Trusted Types once the framework supports them without inline allowances.
- Collect violation reports, which needs an endpoint this static site does not have.