fix(web): add nonce-based CSP

Generate a per-response nonce for SSR scripts instead of permitting arbitrary inline scripts. Allow only required analytics and configured CMS media origins.
This commit is contained in:
2026-10-15 23:44:56 +02:00
parent a5539d18d7
commit 4afc510fcc
2 changed files with 45 additions and 2 deletions
+10 -2
View File
@@ -70,6 +70,11 @@ const FONT_PRELOADS: Readonly<Record<Locale, readonly string[]>> = {
],
};
function createCspNonce(): string {
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
return btoa(String.fromCharCode(...bytes));
}
/**
* Resolved once per request. Every route can read the origin and the locale
* off this loader via `matches[0].data` in its own `meta`.
@@ -88,6 +93,7 @@ export function loader({ request }: Route.LoaderArgs) {
guesses, so there is no flash-of-wrong-theme and no hydration mismatch.
`:root` is dark, so the absence of a cookie is a dark first visit. */
theme: readTheme(request.headers.get("Cookie")),
nonce: createCspNonce(),
};
}
@@ -157,6 +163,7 @@ export function Layout({ children }: { children: ReactNode }) {
surface in either theme. */
const rootData = useRouteLoaderData<typeof loader>("root");
const theme: AppTheme = rootData?.theme ?? DEFAULT_THEME;
const nonce = rootData?.nonce;
const navSurface = useRouteHandleValue(readNavSurface) ?? theme;
const footerSurface = useRouteHandleValue(readFooterSurface) ?? theme;
@@ -208,6 +215,7 @@ export function Layout({ children }: { children: ReactNode }) {
src="https://www.googletagmanager.com/gtag/js?id=G-TJT6NM2DMT"
/>
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
@@ -243,8 +251,8 @@ gtag('config', 'G-TJT6NM2DMT');`,
linkGroups={footerLinkGroups}
/>
<ScrollRestoration />
<Scripts />
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
</html>
);