From a7ea86cdcf2aa3cbb1486baabc4c9d6aea8c576c Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 5 Aug 2026 18:36:36 -0500 Subject: [PATCH 01/16] fix(desktop): enable the content security policy (#4614) This change enables a Tauri content security policy that limits executable content to the packaged application and does not allow inline scripts. Relay, media, asset, and Tauri IPC schemes remain available for desktop compatibility. The policy contains the impact of a future renderer injection; it does not itself remove an injection bug. ## Testing - `git diff --check origin/main...codex/security-desktop-csp` - Rebased onto `origin/main` at `5c98932` - Full CI pending Originating Buzz thread: `buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1` --------- Signed-off-by: Jordan Mecom Signed-off-by: Eli Foster Co-authored-by: Eli Foster Co-authored-by: Claude Opus 5 --- desktop/src-tauri/tauri.conf.json | 2 +- desktop/src-tauri/tests/csp.rs | 201 ++++++++++++++++++ .../profile/lib/animatedAvatarCapture.ts | 5 +- 3 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 desktop/src-tauri/tests/csp.rs diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 0d4417a754..39950d5902 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -36,7 +36,7 @@ ], "macOSPrivateApi": true, "security": { - "csp": null + "csp": "default-src 'self'; base-uri 'self'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; script-src 'self' 'wasm-unsafe-eval' https://cdn.jsdelivr.net/npm/@mediapipe/; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost buzz-media: http://buzz-media.localhost https: http: wss: ws:; img-src 'self' buzz-media: http://buzz-media.localhost data: blob: https: http:; media-src 'self' buzz-media: http://buzz-media.localhost data: blob: https: http:; worker-src 'self' blob:" } }, "plugins": { diff --git a/desktop/src-tauri/tests/csp.rs b/desktop/src-tauri/tests/csp.rs new file mode 100644 index 0000000000..a8cc880e41 --- /dev/null +++ b/desktop/src-tauri/tests/csp.rs @@ -0,0 +1,201 @@ +//! Guards on the packaged-app Content-Security-Policy in `tauri.conf.json`. +//! +//! The CSP is only enforced on assets Tauri itself serves, so neither +//! `just dev` (loads the Vite `devUrl`) nor the Playwright suite (runs under +//! `vite preview`) can catch a policy that breaks the app. These tests pin the +//! non-obvious sources the frontend actually needs, so a future tightening +//! fails here instead of in a signed build. +//! +//! Kept as an integration test so the policy can be checked without the app +//! crate having to declare a test-only module. + +use std::collections::HashMap; + +const TAURI_CONF: &str = include_str!("../tauri.conf.json"); + +fn csp_directives() -> HashMap> { + let conf: serde_json::Value = + serde_json::from_str(TAURI_CONF).expect("tauri.conf.json is valid JSON"); + let csp = conf["app"]["security"]["csp"] + .as_str() + .expect("app.security.csp is set as a policy string"); + + csp.split(';') + .filter_map(|directive| { + let mut parts = directive.split_whitespace(); + let name = parts.next()?; + Some((name.to_owned(), parts.map(str::to_owned).collect())) + }) + .collect() +} + +fn sources(directive: &str) -> Vec { + csp_directives() + .remove(directive) + .unwrap_or_else(|| panic!("csp is missing the {directive} directive")) +} + +#[test] +fn script_src_allows_wasm_instantiation() { + // Shiki's default engine (Oniguruma) instantiates inlined WebAssembly for + // every code block; MediaPipe selfie segmentation does the same. Without + // this token both silently degrade — highlighting drops to plain text and + // animated avatars keep their background. + assert!(sources("script-src").contains(&"'wasm-unsafe-eval'".to_owned())); +} + +/// The `MEDIAPIPE_WASM_BASE` literal the frontend hands to `FilesetResolver`. +fn mediapipe_wasm_base() -> String { + const CAPTURE: &str = include_str!("../../src/features/profile/lib/animatedAvatarCapture.ts"); + + let after = CAPTURE + .split_once("const MEDIAPIPE_WASM_BASE =") + .expect("animatedAvatarCapture.ts declares MEDIAPIPE_WASM_BASE") + .1; + let url = after + .split_once('"') + .expect("MEDIAPIPE_WASM_BASE is a double-quoted string literal") + .1; + url.split_once('"') + .expect("MEDIAPIPE_WASM_BASE literal is terminated") + .0 + .to_owned() +} + +/// The npm scope the MediaPipe loader must come from. A CSP source ending in +/// `/` is a path *prefix* — paths can't be wildcarded — so this admits any +/// `@mediapipe` package while excluding the rest of what jsDelivr serves. +const MEDIAPIPE_SCOPE: &str = "https://cdn.jsdelivr.net/npm/@mediapipe/"; + +#[test] +fn script_src_scopes_the_mediapipe_loader() { + // `FilesetResolver.forVisionTasks` loads `vision_wasm[_nosimd]_internal.js` + // via a `