- Status: Superseded (2026-08-21)
- Date: 2026-06-10
- Revised: 2026-07-28 β corrected a false Consequences claim (see below); no change to the Decision.
- Deciders: Maintainer + Claude Code
- Context tags: security, csp, networking, byok, tauri
Current policy: The historical BYOK tradeoff below was superseded by the explicit cross-surface allowlist in
config/csp-connect-src.json. Web and Tauri share the same enumerated origins. Runtime endpoint preflight rejects an unlisted custom origin with an actionable error; arbitrary custom HTTPS endpoints are not a supported browser transport until a controlled proxy/native transport exists. Locally,pnpm run csp:syncregenerates the surfaces andpnpm run csp:checkvalidates them; CI usespnpm run csp:verifyto regenerate, fail on generated drift, and rejecthttps:,http:,ws:, andwss:wildcards.upgrade-insecure-requestsis intentionally absent: loopback HTTP is potentially trustworthy, but the global directive would still make the explicit HTTP-loopback exception policy dependent on scheme-upgrade behavior instead of keeping that exception visible and mechanically checked.
The web PWA's index.html Content-Security-Policy connect-src directive contained both a
https: scheme-source and an explicit allowlist of cloud-provider endpoints
(https://generativelanguage.googleapis.com, https://api.openai.com, https://api.x.ai,
https://api.groq.com, https://openrouter.ai, https://api.openrouter.ai).
A scheme-source of https: matches every HTTPS origin. So the explicit per-provider entries
allowed nothing the https: source did not already allow β they were dead allowlist entries that
visually implied an egress hardening the policy did not actually provide. A 2026-06-09 audit (F-2)
flagged the explicit list, added in commit 364025e, as functionally inert: the data-exfiltration
vector it appeared to close (an attacker forcing a fetch to an arbitrary HTTPS endpoint, e.g. via
AI prompt injection) remained open for all HTTPS targets.
The obvious "fix" β drop https: and keep only the explicit allowlist β changes a shipped
feature's supported endpoint set. WorldScript ships BYOK (bring-your-own-key) with a user-configurable
openAiCompatibleBaseUrl (Settings β AI β custom base URL; see features/settings/settingsSlice.ts,
components/settings/AiProviderCard.tsx, services/ai/worldScriptCompletionFetch.ts,
services/aiProviderService.ts, types.ts). Users point the app at arbitrary self-hosted or
third-party OpenAI-compatible proxies. Those origins are user data β they cannot be statically
enumerated in a <meta> CSP shipped in the bundle. The current product therefore rejects an
unlisted custom proxy before fetch and explains that the origin needs an explicit policy update;
it does not fail as an opaque network error.
The native Tauri build uses the same declared origin set: src-tauri/tauri.conf.json ships a strict
connect-src with no https: blanket (only the enumerated provider + localhost + wss origins).
The desktop app has OS-level egress controls and a narrower threat model, so the broad scheme is not
needed there.
Option B β keep https:, remove the redundant explicit cloud endpoints, document the tradeoff.
- The web PWA
connect-srckeeps'self' https:plus the sourceshttps:does not cover:http://localhost|127.0.0.1(Ollama :11434, LM Studio :1234, local AI :8000) and the explicitwss://Yjs signaling endpoints. Nohttp:orws:scheme-wildcards. - The explicit
https://β¦provider endpoints are removed β they are strictly redundant underhttps:and their presence misrepresented the policy's strength. - The native Tauri
connect-srcstays strict (nohttps:). The asymmetry is intentional and documented: only the web PWA accepts the broad scheme, and only because of BYOK custom base URLs. - A regression test (
tests/unit/csp.test.ts) asserts the invariant: web CSP containshttps:and nohttp:/ws:scheme-wildcards; Tauri CSP contains nohttps:blanket.
- Positive: the policy now states the truth β
https:is the actual egress boundary, justified by a real shipped feature, instead of an allowlist that implied otherwise. Tauri keeps strict egress.http:/ws:scheme-wildcards remain disallowed, so cleartext exfiltration is still blocked. - Negative (accepted residual risk): an attacker who can drive a
fetchin the web PWA (e.g. successful AI prompt injection into a code path that issues a request) can reach any HTTPS origin. Mitigations: no secrets are placed inconnect-src-reachable globals; API keys are encrypted at rest and only attached to the user-configured provider request; AI output is nevereval'd (AGENTS.mdsecurity red lines). Closing this fully requires build-time CSP generation from the provider registry + a validated custom-endpoint allowlist (Option C), deferred to v2.0. - Revision note (2026-07-28): this section previously claimed "the host (Vercel/CF) tightens CSP
further via HTTP response headers in production." That was false at the time it was written β
none of
vercel.json,public/_headers, ornginx.confset aContent-Security-Policyheader, so the acceptedconnect-srcresidual risk above had no documented compensating control. This has now been fixed:vercel.json,public/_headers, andnginx.confall set a realContent-Security-Policyheader, identical to theindex.htmlmeta CSP (connect-srcis unchanged β this ADR's tradeoff still applies there β butframe-ancestors 'none'is only meaningful as a header, never as a meta tag, so that specific directive is a genuine new hardening on the three hosts that can set it). GitHub Pages β the canonical upstream mirror β cannot set any HTTP response header at all (no_headers-equivalent, no platform config surface); theindex.htmlmeta CSP is the only enforcement point there, andPermissions-Policyhas no meta-tag equivalent at all, so it cannot be set on GitHub Pages under any circumstance. Any future claim in this ADR about host-level hardening must be checked against all four surfaces, not assumed. - Maintenance rule: when adding a new endpoint, update
config/csp-connect-src.json, runpnpm run csp:sync, and extendtests/unit/csp.test.ts; CI runspnpm run csp:verifyto reject drift. Runtime settings must call the shared endpoint preflight before fetch. New header-origin or directive change: updatevercel.json,public/_headers, andnginx.conftogether, plustests/unit/csp.test.tsandtests/unit/deploymentHeaders.test.tsβ a divergence between the header CSP and the meta CSP makes the meta tag misleading (seedocs/DEPLOYMENT.mdΒ§ Header invariants per host).
- Option A β strict allowlist (drop
https:): changes the shippedopenAiCompatibleBaseUrlBYOK feature for users running an unlisted self-hosted or alternate proxy. Accepted with explicit runtime preflight and an actionable unsupported-origin error; arbitrary browser egress is not silently permitted. - Option C β dynamic/build-time CSP generation from the provider registry plus a settings-validated custom endpoint: the correct long-term fix but significant new build machinery; deferred to v2.0.
- Keep the redundant explicit endpoints: rejected β they change nothing under
https:and misrepresent the policy.
index.html(web CSP meta),src-tauri/tauri.conf.json(native CSP)tests/unit/csp.test.ts(regression test)docs/SECURITY-THREAT-MODEL.md(web-vs-Tauri egress asymmetry)- Audit 2026-06-09 finding F-2; commit
364025e - [[0002-local-ai-stack-layering]], [[0003-workerbus-hybrid-routing]]
- See also: [[0013-csp-wasm-and-blob-frames]] β this ADR covers
connect-srconly; thescript-src('wasm-unsafe-eval') andframe-src(blob:) decisions, and the 3-layer CSP test architecture, are recorded separately there to keep each ADR scoped to one directive family.