fix(security): switch CSP to nonce + strict-dynamic (drop unsafe script-src sources)

Mozilla Observatory's rescan flagged the enforced CSP as "implemented
unsafely" (-20) for unsafe-inline/https:/http: in script-src. Enable
Clerk's strict mode, which drops http:/https: and adds a per-request
nonce + 'strict-dynamic' instead. Clerk forwards the resulting CSP
header (with nonce) on both the response and the downstream request,
which is exactly what Next.js's automatic nonce application reads
during SSR, so framework scripts and page bundles are covered for
free. next-themes' hand-written FOUC-prevention <script> is not
covered by that automatic mechanism, so RootLayout now reads the
x-nonce header and passes it to ThemeProvider explicitly.

'unsafe-inline' stays in the header text as a spec-mandated fallback
for pre-CSP3 browsers; strict-dynamic-aware browsers ignore it.
'unsafe-eval' is kept for now (see prior commit ddbeee7) — untouched
by this change, separate follow-up once confirmed unnecessary.
This commit is contained in:
admin committed 2026-08-22 14:39:53 +02:00
1 parent ab1b58d644
commit 9265aa4dc1
2 files changed
+28 -1

No files matched your search

+13 -1
View File
@@ -1,4 +1,5 @@
import type { Metadata, Viewport } from "next";
import { headers } from "next/headers";
import { Inter } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { NextIntlClientProvider } from "next-intl";
@@ -44,6 +45,11 @@ export default async function RootLayout({
}>) {
const locale = await getLocale();
const messages = await getMessages();
// Set by Clerk's contentSecurityPolicy strict mode (src/proxy.ts) — needed
// here because next-themes renders its FOUC-prevention script as a raw
// inline <script>, which Next.js's automatic nonce application does not
// cover (that only applies to scripts Next.js itself generates).
const nonce = (await headers()).get("x-nonce") ?? undefined;
return (
<html
@@ -52,7 +58,13 @@ export default async function RootLayout({
className={`${inter.variable} h-full antialiased font-sans`}
>
<body className="min-h-full flex flex-col">
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
nonce={nonce}
>
<NextIntlClientProvider locale={locale} messages={messages}>
<TRPCReactProvider>
<ActivePetProvider>
+15
View File
@@ -137,6 +137,21 @@ export default clerkMiddleware(
// the eval source is confirmed fixed and remove this line.
contentSecurityPolicy: {
reportOnly: false,
// strict: true drops http:/https: from script-src and adds
// 'strict-dynamic' + a per-request nonce instead (CSP3 best practice —
// Mozilla Observatory flags http:/https:/unsafe-inline as "overly
// broad"). Clerk forwards the resulting Content-Security-Policy header
// (with the nonce) on both the response AND the request it passes
// downstream (see setRequestHeadersOnNextResponse in
// @clerk/nextjs/.../clerkMiddleware.js), which is exactly what
// Next.js's own automatic nonce application reads during SSR — so
// Next's framework scripts and page bundles get noticed for free.
// 'unsafe-inline' stays in the header text as a spec-mandated fallback
// for pre-CSP3 browsers; browsers that understand 'strict-dynamic'
// ignore it. Any hand-written inline <script> (e.g. next-themes'
// FOUC-prevention script in layout.tsx) needs its own nonce prop —
// see RootLayout.
strict: true,
reportTo: "/api/csp-report",
directives: {
"script-src": ["unsafe-eval", "'report-sample'"],