Skip to content

fix(config): replace deploy-web CSP nonce/strict-dynamic with host allowlist - #3523

Open
baktun14 wants to merge 8 commits into
mainfrom
fix/deploy-web-csp-drop-nonce-strict-dynamic
Open

fix(config): replace deploy-web CSP nonce/strict-dynamic with host allowlist#3523
baktun14 wants to merge 8 commits into
mainfrom
fix/deploy-web-csp-drop-nonce-strict-dynamic

Conversation

@baktun14

@baktun14 baktun14 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Why

Sentry DEPLOY-WEB-2JT was escalating — 14,090 report-only CSP violations across 650 users since 2026-07-23, all reporting the app's own Next.js framework chunk (/_next/static/chunks/framework-*.js) as blocked by script-src-elem.

Root cause: the report-only CSP added in #3473 uses a per-request 'nonce-…' + 'strict-dynamic' script policy. deploy-web is Pages Router with statically optimized routes (/, /deployments, …) whose HTML is generated at build time with no nonce. 'strict-dynamic' makes browsers ignore 'self', so every un-nonced first-party script chunk is reported. Nothing is broken today (it's report-only), but flipping CSP_MODE=enforce on this policy would white-screen every static page.

Fixes DEPLOY-WEB-2JT

What

Replace the nonce/strict-dynamic script policy with a fully static policy — host allowlist plus a single script hash — with no 'unsafe-inline':

  • csp.tsscript-src 'self' 'sha256-…' <vendor hosts>; dropped the nonce parameter, generateNonce, 'strict-dynamic', and the redundant www.google-analytics.com entry (covered by *.google-analytics.com).
  • middleware.ts — removed nonce generation, the unused x-nonce header, and the CSP-on-request-header plumbing (NextResponse.next() with the CSP on the response only).
  • TrackingScripts.tsx — GTM no longer bootstraps via the stock inline IIFE: the effect seeds window.dataLayer directly and loads gtm.js as an external allowlisted script. Timing is identical — the IIFE already ran post-hydration from this same effect, so the inline wrapper was pure overhead.
  • domUtils.ts — removed the dead getCspNonce and the innerHTML option from ScriptOptions; the script-injection helper can now only inject external scripts, making inline-script regressions impossible at the type level.
  • AppThemeProvider.tsx — extracted the next-themes provider from _app.tsx. Its inline anti-FOUC theme script is the one remaining inline script (it must run before first paint and is library-rendered, so it can't be externalized) and is allowed by sha256 (THEME_SCRIPT_HASH). AppThemeProvider.spec.tsx recomputes the hash from the rendered provider, so a next-themes upgrade or prop change fails CI with the new hash in the failure message instead of silently breaking production.

Why hash + host allowlist, not nonce or unsafe-inline

  • A nonce cannot work here: statically optimized Pages Router HTML is built once with no request to take a nonce from (the original bug), and any HTML caching layer (next-pwa is enabled) silently re-breaks nonce freshness. The policy must be static to be cache-proof.
  • 'unsafe-inline' would neuter script-src's XSS protection, and both prior inline scripts had clean non-inline solutions — so nothing needs it.

Verification

  • 29 unit tests across csp, middleware, TrackingScripts, and AppThemeProvider (including the hash drift guard); lint and types clean on changed files; full deploy-web unit suite green apart from a pre-existing useProviderJwt failure that reproduces without these changes (passes in CI).
  • Headless Chromium against a local production build (next start, report-only header):
    • / and /deployments (the pages from the Sentry issue): 0 CSP violations — framework chunks and the hashed theme script all execute, and the theme class/color-scheme are applied (no FOUC regression).
    • A gtm.js script element injected from www.googletagmanager.com: no violation (load permitted by the allowlist).
    • Negative control from example.com: script-src-elem violation reported immediately — the policy is live and reports non-allowlisted sources as expected.

Rollout

Stays report-only (the default — CSP_MODE unset). The report-only window now also validates the GTM container: if a Custom HTML tag ever injects an inline script, it will surface in Sentry as a script-src report before enforcement. Watch Sentry to confirm no new script-src reports, then flip CSP_MODE=enforce in a follow-up.

Summary by CodeRabbit

  • Security
    • Updated Content Security Policy generation to remove nonce- and strict-dynamic-based script allowances, and instead rely on an exported hashed theme inline script plus approved first-party and vendor origins (excluding 'unsafe-inline').
    • CSP headers are now applied directly to responses, preserving enforce vs report-only behavior.
  • Bug Fixes
    • Removed request nonce propagation and related header/DOM nonce handling; newly created scripts no longer attempt to use a CSP nonce.
  • Tests
    • Added/updated Vitest coverage for CSP/middleware behavior, theme hash stability, and tracking-script DOM side effects.
  • New Features
    • Introduced an app-level theme provider wrapper to support CSP-sensitive theming and anti-FOUC handling.

…lowlist

The report-only CSP added in #3473 used a per-request nonce plus
'strict-dynamic'. That is incompatible with deploy-web's statically
optimized Pages Router routes: their HTML is generated at build time
with no nonce, and 'strict-dynamic' makes browsers ignore 'self', so
every first-party script chunk (framework-*.js) was reported. This
flooded Sentry with 14k events / 650 users under DEPLOY-WEB-2JT and
would white-screen every static page if CSP_MODE were set to enforce.

Switch script-src to a host allowlist ('self' 'unsafe-inline' + vendor
hosts) that needs no nonce and behaves identically on static and
dynamic pages, so the policy is safe to enforce. Remove the now-dead
nonce plumbing: generateNonce, the x-nonce request header, and
getCspNonce/nonce application in domUtils.

'unsafe-inline' is an intentional, reviewed tradeoff: the app's GTM
loader injects an inline bootstrap client-side, and the static Pages
Router architecture cannot attach a per-request nonce. The host
allowlist plus object-src 'none', base-uri 'self', frame-ancestors
'none' and form-action 'self' are retained.

Keep CSP_MODE=report-only for now; watch Sentry for any GTM-loaded
origins now that 'strict-dynamic' is gone and add them to the
allowlist before flipping to enforce.

Fixes DEPLOY-WEB-2JT
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e34d3e87-63ea-4797-b760-c77553262a0a

📥 Commits

Reviewing files that changed from the base of the PR and between 0472937 and cbfe59c.

📒 Files selected for processing (2)
  • apps/deploy-web/src/components/layout/TrackingScripts.spec.tsx
  • apps/deploy-web/src/components/layout/TrackingScripts.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/deploy-web/src/components/layout/TrackingScripts.spec.tsx
  • apps/deploy-web/src/components/layout/TrackingScripts.tsx

📝 Walkthrough

Walkthrough

Deploy-web replaces request-based CSP nonces with a hashed theme script, applies CSP headers directly to responses, introduces an app theme provider, and refactors Google Tag Manager bootstrapping with focused tests.

Changes

Deploy-web CSP and script integration

Layer / File(s) Summary
Hashed CSP policy contract
apps/deploy-web/src/lib/csp/csp.ts, apps/deploy-web/src/lib/csp/csp.spec.ts
buildContentSecurityPolicy removes nonce and 'strict-dynamic' sources and includes THEME_SCRIPT_HASH; tests validate the updated script-src directives.
Middleware response header wiring
apps/deploy-web/src/middleware.ts, apps/deploy-web/src/middleware.spec.ts
Middleware builds CSP without a nonce and applies headers directly to the response; tests cover report-only, enforce, and absent x-nonce behavior.
Theme provider and hash validation
apps/deploy-web/src/components/layout/AppThemeProvider.tsx, apps/deploy-web/src/components/layout/AppThemeProvider.spec.tsx, apps/deploy-web/src/pages/_app.tsx
Adds the configured AppThemeProvider, uses it in AppRoot, and verifies its inline script matches the CSP hash.
Tracking script bootstrapping
apps/deploy-web/src/components/layout/TrackingScripts.tsx, apps/deploy-web/src/components/layout/TrackingScripts.spec.tsx
Moves GTM loading to external script and noscript helpers with injectable dependencies, while preserving production and tracking gates and testing the behavior.
Script element nonce removal
apps/deploy-web/src/utils/domUtils.ts
createScriptElement() no longer reads or assigns a CSP nonce.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: ygrishajev

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/deploy-web-csp-drop-nonce-strict-dynamic

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.42%. Comparing base (25bba7b) to head (cbfe59c).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3523      +/-   ##
==========================================
- Coverage   74.18%   73.42%   -0.76%     
==========================================
  Files        1142     1053      -89     
  Lines       29723    27371    -2352     
  Branches     7441     6981     -460     
==========================================
- Hits        22049    20098    -1951     
+ Misses       6779     6408     -371     
+ Partials      895      865      -30     
Flag Coverage Δ *Carryforward flag
api 87.41% <ø> (+0.01%) ⬆️ Carriedforward from 0472937
deploy-web 64.03% <100.00%> (+0.35%) ⬆️
log-collector ?
notifications 93.84% <ø> (ø) Carriedforward from 0472937
provider-console 81.38% <ø> (ø) Carriedforward from 0472937
provider-inventory ?
provider-proxy 88.17% <ø> (ø) Carriedforward from 0472937
tx-signer ?

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...loy-web/src/components/layout/AppThemeProvider.tsx 100.00% <100.00%> (ø)
...ploy-web/src/components/layout/TrackingScripts.tsx 100.00% <100.00%> (+90.90%) ⬆️
apps/deploy-web/src/lib/csp/csp.ts 100.00% <100.00%> (+7.54%) ⬆️
apps/deploy-web/src/middleware.ts 59.52% <100.00%> (+59.52%) ⬆️
apps/deploy-web/src/utils/domUtils.ts 87.09% <ø> (+14.87%) ⬆️

... and 96 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/deploy-web/src/lib/csp/csp.spec.ts (1)

53-62: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the new Google Analytics allowlist entries.

The policy adds https://www.google-analytics.com and https://*.google-analytics.com, but this test only protects GTM and Stripe. A regression removing either GA source would still pass.

Suggested assertions
       expect(scriptSrc).toContain("https://www.googletagmanager.com");
       expect(scriptSrc).toContain("https://js.stripe.com");
+      expect(scriptSrc).toContain("https://www.google-analytics.com");
+      expect(scriptSrc).toContain("https://*.google-analytics.com");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/deploy-web/src/lib/csp/csp.spec.ts` around lines 53 - 62, Update the
test case “allows first-party and vendor scripts via a host allowlist without a
nonce or strict-dynamic” to assert that scriptSrc contains both
https://www.google-analytics.com and https://*.google-analytics.com, alongside
the existing GTM and Stripe allowlist assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/deploy-web/src/lib/csp/csp.ts`:
- Around line 82-88: Update buildContentSecurityPolicy so enforce mode does not
emit "'unsafe-inline'" in script-src; retain report-only behavior until required
inline scripts are covered by hashes or nonces, or construct a separate enforce
policy without that directive.

---

Nitpick comments:
In `@apps/deploy-web/src/lib/csp/csp.spec.ts`:
- Around line 53-62: Update the test case “allows first-party and vendor scripts
via a host allowlist without a nonce or strict-dynamic” to assert that scriptSrc
contains both https://www.google-analytics.com and
https://*.google-analytics.com, alongside the existing GTM and Stripe allowlist
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4d9e58ef-ab97-4163-a980-dd0de6036189

📥 Commits

Reviewing files that changed from the base of the PR and between 627ad0c and e855ea0.

📒 Files selected for processing (4)
  • apps/deploy-web/src/lib/csp/csp.spec.ts
  • apps/deploy-web/src/lib/csp/csp.ts
  • apps/deploy-web/src/middleware.ts
  • apps/deploy-web/src/utils/domUtils.ts
💤 Files with no reviewable changes (1)
  • apps/deploy-web/src/utils/domUtils.ts

Comment thread apps/deploy-web/src/lib/csp/csp.ts
middleware.ts had no test, so the CSP-header lines changed in this PR
showed 0% patch coverage and failed codecov/patch/deploy-web (which in
turn failed validate/result and the security-scan gate).

Add middleware.spec.ts asserting the middleware attaches the CSP header
(report-only by default, enforcing under CSP_MODE=enforce) and no longer
emits an x-nonce header.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/deploy-web/src/middleware.spec.ts`:
- Around line 11-16: Expand the test for the default response created by setup
in middleware.spec.ts to assert the complete approved script-src CSP allowlist,
including all required vendor and GA hosts. Also assert that the policy excludes
'unsafe-inline', 'strict-dynamic', and nonce sources, while preserving the
existing report-only header and absent enforcing-header checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 585262fa-2558-45cb-a58d-240193aae50e

📥 Commits

Reviewing files that changed from the base of the PR and between e855ea0 and 8ee729a.

📒 Files selected for processing (1)
  • apps/deploy-web/src/middleware.spec.ts

Comment thread apps/deploy-web/src/middleware.spec.ts
baktun14 added 5 commits July 27, 2026 14:37
Strengthen the CSP contract tests so a regression is caught: assert every
approved script-src host (GTM, GA, growth-channel, cloudflare, stripe) and
that 'strict-dynamic'/nonce sources stay absent. The middleware test now
rejects those directives too, instead of a loose 'self' substring check.
@github-actions github-actions Bot added size: M and removed size: S labels Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/deploy-web/src/components/layout/TrackingScripts.tsx`:
- Around line 15-38: Guard loadGoogleTagManager and appendGtmNoscriptFallback
against a missing gtmId before injecting the Google Tag Manager script or
iframe. Return without modifying the data layer or DOM when NEXT_PUBLIC_GTM_ID
is unset, while preserving the existing loading behavior for valid IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03c1d15c-75b3-4fae-8589-aa92c8ccb246

📥 Commits

Reviewing files that changed from the base of the PR and between 767a0bb and 0472937.

📒 Files selected for processing (9)
  • apps/deploy-web/src/components/layout/AppThemeProvider.spec.tsx
  • apps/deploy-web/src/components/layout/AppThemeProvider.tsx
  • apps/deploy-web/src/components/layout/TrackingScripts.spec.tsx
  • apps/deploy-web/src/components/layout/TrackingScripts.tsx
  • apps/deploy-web/src/lib/csp/csp.spec.ts
  • apps/deploy-web/src/lib/csp/csp.ts
  • apps/deploy-web/src/middleware.spec.ts
  • apps/deploy-web/src/pages/_app.tsx
  • apps/deploy-web/src/utils/domUtils.ts
💤 Files with no reviewable changes (1)
  • apps/deploy-web/src/utils/domUtils.ts

Comment thread apps/deploy-web/src/components/layout/TrackingScripts.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant