From 3e058af40a5bf6a2b676130a8149cc1b489d2173 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:40:10 +0200 Subject: [PATCH 1/8] fix(security): swap script-src 'unsafe-inline' for a per-request nonce MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hash version was built, measured and rejected a day earlier (#11213): Cloudflare JavaScript Detections injects an inline script at the edge whose body carries a per-response ray id, so it has no hash that can be listed, and a hash-only script-src blocked exactly that one script. Cloudflare documents the way out — it copies a nonce out of the response header onto the script it injects — and that is what this ships. nginx mints the nonce from $request_id (16 random bytes as 32 hex digits, a subset of the CSP nonce charset), sends it in the policy and stamps the same value onto every + ships to normal users. A classic script element (not type="module") so + it runs before module parse-time errors and can capture them — critical + for debugging mobile-only crashes where remote DevTools aren't + available. The opening tag is described rather than written out: nginx + stamps the CSP nonce with a blunt `sub_filter` over the literal string, + so a comment containing it comes back to the browser with a stray + `nonce=` inside — harmless, but confusing in the one artefact anyone + debugging CSP will read. --> `, and sub_filter's default type list is + # text/html alone, so this never reaches into a .js chunk or a JSON body. + # It does also pass over the proxied crawler pages from @seo_proxy, which + # carry no executable script of ours — one JSON-LD data block, which no + # browser executes — and crawlers run no JS either way. + sub_filter '.gz` untouched when it exists, which is exactly what + # sub_filter cannot rewrite. index.html is therefore excluded from + # precompression in app/vite.config.ts — the nonce depends on it. gzip_static on; # Vite-fingerprinted assets — filename changes on every build, safe to cache forever @@ -380,6 +404,13 @@ server { # main server block above). include /etc/nginx/security-headers.conf; + # CSP nonce, same as the main server block — and this block is the reason + # it sits at server level: the two spec-route regex locations below serve + # `/index.html` through `try_files` as a FILE, so they never reach the + # `location = /index.html` block and would carry no stamp at all. + sub_filter '