From 38dd1129e59b0cb688a910d17ce7a47b530f0f00 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 16 Aug 2026 22:58:53 +0200 Subject: [PATCH] feat(security): set a restrictive CSP and scope the asset protocol to thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `app.security.csp` was `null`, so the webview ran with no Content-Security-Policy at all: any script that reached the web layer would have inherited the whole IPC surface. There is no known injection path today (one app-owned `{@html}`, no `innerHTML`/`eval`), so this is defence in depth rather than a fix for an open hole. `script-src 'self'` is the restrictive half — Tauri nonces SvelteKit's inline bootstrap script at build time, so no `'unsafe-inline'` is needed — together with `object-src`/`frame-src 'none'` and `base-uri 'self'`. `img-src`/`media-src`/ `connect-src` cannot be restrictive: the Jellyfin origin is typed in by the user at run time and is routinely plain http on a LAN, so they allow `http:`/`https:`. That is a wide grant for data, but it still bars `file:`/`filesystem:` and does not touch script execution. A run-time policy naming the server exactly was rejected: Tauri derives the header from immutable config when it serves the HTML, so it would mean rebuilding config and reloading the webview on every server change. `style-src` keeps `'unsafe-inline'` because Svelte compiles `style="…"` attributes into markup; `worker-src`/`media-src` keep `blob:` for hls.js's demuxer worker and its MSE object URL; `ipc:`/`http://ipc.localhost` keeps `invoke` working. `devCsp` mirrors it with the eval/inline/websocket allowances Vite's dev server needs. The asset-protocol scope narrows from `$APPDATA/**` — the storage root holding the SQLite database and the encrypted-token fallback file — to `$APPDATA/thumbnails/**`. Since DR-137 moved downloaded media to the loopback media server, `imageCache` is the only `convertFileSrc` caller left. Needs manual verification on both platforms: thumbnails, online HLS video and offline downloaded video cannot be exercised headlessly. --- docs/architecture/09-security.md | 61 + docs/requirements.md | 8 +- docs/traceability.md | 8360 +++++++++++++------------ scripts/extract-traces.test.ts | 4 +- scripts/tauri-security-config.test.ts | 92 + src-tauri/Cargo.toml | 14 +- src-tauri/src/lib.rs | 23 +- src-tauri/tauri.conf.json | 5 +- src/lib/services/imageCache.ts | 5 +- 9 files changed, 4391 insertions(+), 4181 deletions(-) create mode 100644 scripts/tauri-security-config.test.ts diff --git a/docs/architecture/09-security.md b/docs/architecture/09-security.md index b54309ca..ec4ec3ce 100644 --- a/docs/architecture/09-security.md +++ b/docs/architecture/09-security.md @@ -51,6 +51,66 @@ pub struct EncryptedFileStorage; // AES-256-GCM fallback | Token Transmission | Bearer token in `Authorization` header only | | Token Refresh | Handled by Jellyfin server (long-lived tokens) | +## Webview Content Security Policy + +`app.security.csp` in `tauri.conf.json` (TRACES: UR-012, UR-071 | DR-198). It was +`null` — CSP disabled — which meant any script that reached the web layer +inherited the full IPC surface. Tauri computes the header from this value when it +serves the embedded HTML, injecting a nonce for SvelteKit's inline bootstrap +script, so `script-src` needs no `'unsafe-inline'`. + +``` +default-src 'self'; +script-src 'self'; +style-src 'self' 'unsafe-inline'; +font-src 'self' data:; +img-src 'self' data: blob: asset: http://asset.localhost http: https:; +media-src 'self' blob: asset: http://asset.localhost http://127.0.0.1:* http: https:; +connect-src 'self' ipc: http://ipc.localhost http: https:; +worker-src 'self' blob:; +object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none' +``` + +| Directive | Why | +|-----------|-----| +| `default-src 'self'` | Everything not named below is same-origin only. | +| `script-src 'self'` | The genuinely restrictive half. Bundled JS only; Tauri's build-time nonce covers the one inline `