Skip to content
COMP10001Playground
All decision records

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 in web/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' and frame-ancestors 'none'.
    • script-src 'self' 'unsafe-inline' and style-src 'self' 'unsafe-inline', with 'unsafe-eval' added in development only.
    • X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin.
  • A unit test checks that connect-src lists exactly this site and the provider origins.

Options considered

  1. No policy. Simplest, and the status quo, but no limit on where a key could go.
  2. 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.
  3. 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.
  4. 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 with fetch or 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 fetch to 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.