fix(desktop): externalize boot <style> to prevent Tauri CSP nonce override - #5242
Conversation
Tauri's build-time asset processor finds the inline <style> in index.html, computes its SHA256, and injects a 'sha256-…' source into style-src at compile time. Per the CSP spec, once a hash or nonce is present in a directive the browser ignores 'unsafe-inline' for that directive — so TipTap's runtime createStyleTag call and emoji-mart's shadow-root style injection were both blocked in packaged builds, manifesting as two v0.5.6 regressions: • Tab-complete caret landing inside @name instead of after the trailing space (whitespace collapses without TipTap's white-space: break-spaces stylesheet). • Emoji picker rendering unstyled/janky. Fix: move html { background-color: #000; } from an inline <style> to desktop/public/boot.css and link it from <head>. A linked stylesheet is not subject to Tauri's build-time hash injection, so 'unsafe-inline' in style-src applies as written. The stylesheet is render-blocking, so boot-flash behaviour is identical to before. Also add devCsp matching the production policy with dev-appropriate script-src allowances so future packaged-only CSP regressions are visible in the csp.rs tests even before a release build. The production CSP string itself is unchanged; the security policy is unaffected. Will's follow-up with the security team (Jordan Mecom / Eli Foster, authors of #4614) is noted for post-ship. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Thufir pass-1 corrections: - Replace sha256 with nonce throughout comments: Tauri nonces inline <style> elements (not SHA-256 hashes them); only inline scripts get the SHA-256 treatment. - Remove devCsp field from tauri.conf.json: Tauri only applies devCsp to assets it serves via its custom protocol; the Buzz dev webview navigates directly to Vite's devUrl, so the field has no effect. Dev CSP detection is deferred to a separate Tauri-faithful Vite middleware follow-up. - Deduplicate rationale: boot-order context in index.html (one line pointer to boot.css), full externalization reason in boot.css, script-policy invariant only in csp.rs. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
|
Independent release verdict by Carl on Wes’s behalf at exact head I independently traced the failure through the versions actually locked by this head:
The fix removes the only real inline Security precision: this does restore the effective Validation at this exact head: typecheck passed; Biome passed on changed web assets; Vite production build passed and emitted Non-blocking coverage gap: |
…format * origin/main: (60 commits) feat(desktop): unify add agent flows (#5015) fix(buzz-agent): budget summarizer reasoning separately so it cannot starve the handoff summary (#5248) infra: bind development services to loopback (#4871) chore(release): release Buzz Desktop version 0.5.7 (#5252) fix(desktop): isolate relay admission tests (#5221) fix(desktop): externalize boot <style> to prevent Tauri CSP nonce override (#5242) fix(desktop): let imported and recovered identities finish onboarding (#5228) Recover from max-token response truncation (#5223) chore(release): release Buzz Desktop version 0.5.6 (#5214) fix(mobile): keep latest messages above composer (#4981) fix(sdk): preserve self-mention p tags in message and forum event builders (#4975) bump @tauri-apps/cli to ~2.11.4 to fix linux app icon issue (#4858) feat(desktop): adding rich link previews to messages (#3818) fix(buzz-agent): Responses reasoning summary, Anthropic display:summarized, ACP v2 messageId (#5195) fix(desktop): retain distinct agent instances in autocomplete (#5202) fix(desktop): defer channel visibility change to Save (#5203) feat(desktop): Projects follow-ups — access restrictions, fast loading, activity feed polish (#5073) refactor(cli): replace probe/decider/detail split with single typed extractor (#5191) fix(desktop): drop unhandled rejection from throwing window.Notification (#5143) fix(desktop): fence localStorage SecurityError from killing the React tree (#5142) ... Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
* origin/main: fix(desktop): welcome banner overlap and missing dismiss control (#5330) fix(desktop): prevent horizontal clipping in Prompt Context modal (#5324) chore(release): release Buzz Relay version 0.2.1 (#2856) chore(release): release Buzz Desktop version 0.5.8 (#5326) fix(buzz-agent): recover from 400-shaped image rejections; unbound benchmark agent rounds (#5318) Revert "fix(acp): reject unattended permission requests" (#5323) feat(desktop): unify add agent flows (#5015) fix(buzz-agent): budget summarizer reasoning separately so it cannot starve the handoff summary (#5248) infra: bind development services to loopback (#4871) chore(release): release Buzz Desktop version 0.5.7 (#5252) fix(desktop): isolate relay admission tests (#5221) fix(desktop): externalize boot <style> to prevent Tauri CSP nonce override (#5242) fix(desktop): let imported and recovered identities finish onboarding (#5228) Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Problem
Two v0.5.6-only regressions were introduced by #4614 (the first enforced Tauri CSP):
Tab-complete caret regression — after tab-completing an @mention, #channel, or :emoji: shortcode, the cursor landed inside the inserted text instead of after the trailing space. TipTap inserts the correct text including the trailing space, but without its base stylesheet (
.ProseMirror { white-space: break-spaces }) the trailing space collapses visually and the caret appears mid-name.Emoji picker unstyled — the emoji-mart picker rendered as a giant unstyled layout (oversized search SVG, collapsed grid) because emoji-mart's shadow-root stylesheet injection was also blocked.
Both symptoms have the same root cause.
Root Cause
Tauri's build-time asset processor scans
index.htmlfor inline<style>elements, injects a nonce token, and adds the corresponding'nonce-…'source tostyle-srcat runtime. Per the CSP spec, once a nonce is present in a directive, the browser ignores'unsafe-inline'for that directive.index.htmlcontained an inline<style>with the boot background color. When Tauri nonced it and injected'nonce-…'intostyle-src, the intendedstyle-src 'self' 'unsafe-inline'became effectivelystyle-src 'self' 'nonce-…'— blocking any runtime stylesheet injection not covered by a matching nonce:injectCSS()→createStyleTag()injecting.ProseMirror { white-space: break-spaces; … }document.createElement('style')injection(Inline scripts follow a separate path — they are SHA-256 hashed, not nonced.)
This only reproduces in packaged builds (where Tauri's custom protocol serves the HTML and enforces the policy).
tauri devloads from the Vite dev server and is not affected.Fix
Move
html { background-color: #000; }from an inline<style>inindex.htmltodesktop/public/boot.css, linked via<link rel="stylesheet">. A linked stylesheet is not subject to Tauri's nonce injection, so'unsafe-inline'instyle-srcapplies as declared.The
<link>is render-blocking (same as the inline style was), so boot-flash behaviour is identical.The production CSP string is unchanged. This fix makes the policy apply as intended — no security properties are altered. Will's follow-up with the security team (Jordan Mecom / Eli Foster, authors of #4614) is noted for post-ship.
A Tauri-faithful CSP harness for the Vite dev path (so this class of regression is visible before a packaged build) is tracked as a separate follow-up.
Files Changed
desktop/index.html— replace inline<style>with<link rel="stylesheet" href="/boot.css" />desktop/public/boot.css— new file, the extractedhtml { background-color: #000; }plus rationale commentdesktop/src-tauri/tests/csp.rs— update comment: nonce for styles, SHA-256 for the boot scriptTesting
just desktop-typecheck✅just desktop-test✅ (4535/4535)just desktop-tauri-test✅ (all Rust tests includingcsp.rs)pnpm tauri build --debugcompleted; compiled binary bakesstyle-src 'self' 'unsafe-inline'with no nonce source injected ✅