fix(desktop): externalize boot <style> to prevent Tauri CSP nonce override (#5242)

## Problem

Two v0.5.6-only regressions were introduced by #4614 (the first enforced
Tauri CSP):

1. **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.

2. **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.html` for inline
`<style>` elements, injects a nonce token, and adds the corresponding
`'nonce-…'` source to `style-src` at runtime. Per the CSP spec, **once a
nonce is present in a directive, the browser ignores `'unsafe-inline'`
for that directive**.

`index.html` contained an inline `<style>` with the boot background
color. When Tauri nonced it and injected `'nonce-…'` into `style-src`,
the intended `style-src 'self' 'unsafe-inline'` became effectively
`style-src 'self' 'nonce-…'` — blocking any runtime stylesheet injection
not covered by a matching nonce:

- TipTap's `injectCSS()` → `createStyleTag()` injecting `.ProseMirror {
white-space: break-spaces; … }`
- emoji-mart's shadow-root `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 dev` loads from the
Vite dev server and is not affected.

## Fix

Move `html { background-color: #000; }` from an inline `<style>` in
`index.html` to `desktop/public/boot.css`, linked via `<link
rel="stylesheet">`. A linked stylesheet is not subject to Tauri's nonce
injection, so `'unsafe-inline'` in `style-src` applies 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 extracted `html {
background-color: #000; }` plus rationale comment
- `desktop/src-tauri/tests/csp.rs` — update comment: nonce for styles,
SHA-256 for the boot script

## Testing

- `just desktop-typecheck` 
- `just desktop-test`  (4535/4535)
- `just desktop-tauri-test`  (all Rust tests including `csp.rs`)
- Packaged validation: `pnpm tauri build --debug` completed; compiled
binary bakes `style-src 'self' 'unsafe-inline'` with no nonce source
injected 

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This commit is contained in:
Will Pfleger
2026-08-07 15:05:38 -07:00
committed by GitHub
co-authored by Duncan
parent a5a9240241
commit dcc1231d6d
3 changed files with 27 additions and 6 deletions
+5 -5
View File
@@ -12,6 +12,10 @@
in tauri.conf.json. The body paints its own themed background once the app
CSS loads, so this never shows through after boot.
The linked stylesheet (boot.css) applies the initial black background.
Kept as a <link> rather than an inline <style> to avoid Tauri's nonce
injection for style-src (see boot.css for the full rationale).
The inline script below reads the cached theme background (same
`buzz-theme-cache` entry ThemeProvider writes) and applies it synchronously
so the boot color matches the themed loading gate — no black flash on light
@@ -20,11 +24,7 @@
that ThemeProvider applies moments later — no wrong-scheme flash before
it loads.
-->
<style>
html {
background-color: #000;
}
</style>
<link rel="stylesheet" href="/boot.css" />
<script>
(() => {
var cached, bg, parsed;
+18
View File
@@ -0,0 +1,18 @@
/*
* Boot background — applied before the app bundle loads so the window never
* flashes an unstyled background on cold start. The inline script in
* index.html overwrites this with the cached theme color once ThemeProvider's
* state is available; on first-ever launch it falls back to this black default.
*
* Kept as a linked stylesheet (not an inline <style>) so that Tauri's
* build-time asset processing does not inject a nonce token into this element.
* Tauri nonces inline <style> elements and adds the corresponding 'nonce-…'
* source to style-src at runtime; per the CSP spec a nonce in a directive
* causes the browser to ignore 'unsafe-inline', which would block TipTap's
* runtime stylesheet injection and emoji-mart's shadow-root styles in packaged
* builds. The inline boot script is SHA-256 hashed (not nonced) — that path
* only applies to scripts, not stylesheets.
*/
html {
background-color: #000;
}
+4 -1
View File
@@ -195,7 +195,10 @@ fn connect_src_allows_ipc_and_cleartext_relays() {
fn script_src_stays_free_of_unsafe_inline_and_eval() {
let allowed = sources("script-src");
// The inline boot script in index.html is covered by Tauri's build-time
// sha256 hashing, so neither escape hatch is ever needed here.
// SHA-256 hashing (scripts only — Tauri nonces inline <style> elements via
// a different path), so neither escape hatch is ever needed here. The boot
// background style was moved to public/boot.css to avoid the nonce path for
// style-src; see boot.css for the full rationale.
assert!(!allowed.contains(&"'unsafe-inline'".to_owned()));
assert!(!allowed.contains(&"'unsafe-eval'".to_owned()));
}