React Router v7 CSP (Content Security Policy) Configuration

Y
Sun Jul 27 2025·37 min read

CSP References: Content Security Policy (CSP) - HTTP | MDN, Content Security Policy - OWASP Cheat Sheet Series, Content-Security-Policy (CSP) Header Quick Reference.

React Router References: Middleware#response-headers | React Router, Security | React Router, Security - Can't use CSP that blocks 'unsafe-inline'; · Issue #183 · remix-run/remix

Why Should You Consider CSP

We are implementing CSP (Content Security Policy) for our website. This is a basic header setting to prevent XSS attack, and is widely implemented. However, I cannot find a solid and promising way to get the nonce set in React Router v7 - framework mode, and really lack of documentation.

This post will walk through the steps to set up CSP via middleware and loader in React Router v7.

Important Steps:

  1. Generate nonce and pass to context in either root loader() or middleware.

  2. entry.server.tsx: React Router uses some inline script for DX, so we should get nonce from the context here and pass it to these scripts. p.s. middleware[] → loader() → handleRequest().

  3. Add nonce meta tag

Generate Nonce in Root Loader or Middleware

They are both capable of configuring headers, if you don’t want to use future middleware flag, just return headers with data() in root loader. However, I use unstable_createContext() to share nonce in server.

Please remember to set nonce in the context either in loader or in middleware!

Create Nonce Context

// context/csp-nonce.ts
export const nonceContext = unstable_createContext<string>()

In root loader()

// root.tsx
export const headers = ({ loaderHeaders }: Route.HeadersArgs) => {
  return loaderHeaders;
};

export const loader = ({ context }: Route.LoaderArgs) => {
  const nonce = generateNonce();
  const headers = {
    [process.env.NODE_ENV === "production"
      ? "Content-Security-Policy"
      : "Content-Security-Policy-Report-Only"]: getContentSecurityPolicy(nonce),
    /** @see https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security */
    "Strict-Transport-Security": "max-age=3600", // 1 hour. HTTPS only
    "X-Frame-Options": "SAMEORIGIN", // Prevent clickjacking
    "X-Content-Type-Options": "nosniff", // Prevent MIME type sniffing
  };
  context.set(nonceContext, nonce);
  return data({ nonce }, { headers });
};

In middleware

// root.tsx
const headersMiddleware: Route.unstable_MiddlewareFunction = async (
	{ context, request },
	next,
) => {
	const nonce = generateNonce()

	const headers = {
		[process.env.NODE_ENV === 'production'
			? 'Content-Security-Policy'
			: 'Content-Security-Policy-Report-Only']: getContentSecurityPolicy(nonce),
		/** @see https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security */
		'Strict-Transport-Security': 'max-age=3600', // 1 hour. HTTPS only
		'X-Frame-Options': 'SAMEORIGIN', // Prevent clickjacking
		'X-Content-Type-Options': 'nosniff', // Prevent MIME type sniffing
	}

	context.set(nonceContext, nonce)

	const response = await next()
	for (const [key, value] of Object.entries(headers)) {
		response.headers.set(key, value)
	}

	return response
}

export const unstable_middleware: Route.unstable_MiddlewareFunction[] = [headersMiddleware]

Retrieve nonce from context in entry.server.tsx

Here in the handleRequest() function, we could retrieve nonce context set from loader or middleware, and we should pass the nonce to renderToPipeableStream()

This solves import("/node_modules/.pnpm/@[email protected]_@... in dev and import("/assets/entry.client-BYY3z-UA.js"); in prod.

export default function handleRequest(props) {
  let nonce: string;

  try {
    // If root headers does not set, there will not be a nonce
    nonce = loadContext.get(nonceContext);
  } catch (error) {
    nonce = generateNonce();
    console.warn("No nonce found in context, generating a fallback.");
  }

  // ...
  const { pipe, abort } = renderToPipeableStream(
    <ServerRouter context={routerContext} url={request.url} nonce={nonce} />,
    //                                                      ^ pass nonce to <ServerRouter>
    {
      nonce, // and renderToPipeableStream
      // ...
    }
  );
  // ...
}

Add nonce Meta Tag

Vite will automatically search for csp-nonce meta.

This is quite confusing, in vite documentation they do mentions that vite will inject a meta tag, but only when html.cspNonce is configured (Features | Vite). You could find this script throwing error if you don’t provide a meta tag:

const cspNonce = "document" in globalThis ? document.querySelector("meta[property=csp-nonce]")?.nonce : void 0;

Whatever, so we provide corresponding meta tag in root:

//...
export function Layout({ children }: { children: React.ReactNode }) {
//...
return(
		// ...
		<head>
			<meta charSet="utf-8" />
			<meta name="viewport" content="width=device-width, initial-scale=1" />
+			{import.env.meta.DEV && <meta property="csp-nonce" nonce={nonce} />
			<Meta />
			<Links />
		</head>
		// ...
	)
}

Unsolved Issue: CSS Import in Dev Mode

import "./app.css"; in root is a conventional way to share global style. However, vite directly inject the <style> tag in development mode, causing csp violation. In production css file is bundled and imported by <link> tag <link rel="stylesheet" href="/assets/root-4bMW1FFt.css">), so no error occurs.


Nonce Hydration: Browser Will Clear Nonce Value

Another headache is although I pass the correct nonce value in the server, in the client it always throws hydration error. Value used as content won’t throw error, used in “nonce“ attribute does.

For example:

export const Comp = () => {
  const nonce = useNonce()
  return(
    <>
      <style nonce={nonce}>{nonce}</style> {/* This throws error */}
      <style>{nonce}</style>
    </>
  )
}

The way I solve this is by using remix-utils use-hydrated hook, only renders the nonce after hydrating. remix-utils-use-hydrated

// ...
const isHydrated = useHydrated()
const safeNonce = isHydrated ? nonce : undefined
// ...

kentcdodds/nonce-hydration-issues: This reproduction made by Kent C. Dodds may be really helpful if you encounter this problem.

  • headers
  • middleware
  • nonce

Subscribe to new posts!

If you like topics like Tech, Software Development, or Travel. Welcome to subscribe for free to get some fresh ideas!