Content Security Policy (CSP) in Next.js and styled-components — The Complete Guide
Implementing a Content Security Policy (CSP) is one of the most effective ways to protect your application against XSS attacks. While this…
Content Security Policy (CSP) in Next.js and styled-components — The Complete Guide
Implementing a Content Security Policy (CSP) is one of the most effective ways to protect your application against XSS attacks. While this is straightforward in static HTML, things get significantly more complicated in Server-Side Rendered (SSR) React applications using styled-components.
Why? Because the styled-components library injects <style> tags dynamically in the browser. By default, these tags do not have a "nonce" attribute, causing them to be immediately blocked by the CSP policy.
To fix this, we must build a precise cryptographic token flow through four layers: the proxy server, the document, the DOM model, and the webpack runtime. The following guide demonstrates how to achieve this using Next.js 15+ (Pages Router) and styled-components 6.
Solution Architecture
The secret lies in perfect synchronization. Losing the token at any of these stages will cause the application to lose its styling or flood the console with errors. The flow is as follows:
- The Proxy generates a unique token (nonce) for each request and sets the appropriate headers.
- The
_document.tsxfile (SSR) reads the token, passes it to the tags in the HTML document, and collects styles generated on the server. - The Browser receives the HTML, where Next.js places the token in a special
<noscript data-n-css="NONCE">tag. - The Client-side script reads the token from the DOM and passes it to the webpack environment, where the
styled-componentslibrary retrieves it.
Step 1: Forcing Dynamic Rendering and Its Consequences
This is a step most tutorials overlook, yet it carries colossal changes for the application architecture. A nonce-based CSP policy requires the token to be unique for every request. If Next.js serves a page from a static cache, the token in the HTML will not match the token in the response header, and the browser will block the resources.
According to the official Next.js documentation, this requirement necessitates a transition to full dynamic rendering, which carries serious consequences for your application:
- Loss of Static Optimization and ISR: Features such as Incremental Static Regeneration (ISR) and Static Site Generation (SSG) are completely disabled.
- CDN Caching Issues: Since every server response must contain a new, unique header with a token, dynamically generated pages cannot be cached by default by Edge CDN systems.
- Lack of Partial Prerendering (PPR) Support: The PPR mechanism becomes incompatible with nonce-based CSP because the pre-generated static “shell” will not have access to a real-time generated token.
- Performance Drop and Higher Costs: Moving from static to dynamic rendering means slower initial page loads (every hit requires server work), increased load on the backend infrastructure, and higher hosting costs.
If you consciously accept these trade-offs for the sake of the highest security level, you must force the framework to render every page dynamically. In the Pages Router architecture, simply export the getServerSideProps function from the page file:
// pages/index.tsx
export const getServerSideProps = () => ({ props: {} });
export default function HomePage() {
return <div>Hello, secure world.</div>;
}
Even an empty export is enough to inform Next.js: “this page must be rendered on every request, never cache it.” For multiple subpages, it is convenient to create a reusable export:
// utils/ssr.ts
export const getServerSideProps = () => ({ props: {} });
Then you can import and re-export it in your view files:
export { getServerSideProps } from '@/utils/ssr';
Step 2: Generating the Token in the Proxy
Starting with Next.js 15, the framework introduced the proxy.ts file as an official alternative to the previous middleware.ts. Both files run on the Edge Runtime environment and are processed identically.
Your proxy.ts file must generate a random token and append it to the headers. Create the src/proxy.ts file and paste the following code:
// src/proxy.ts
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const isDev = process.env.NODE_ENV === 'development';
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ''};
style-src 'self' ${isDev ? "'unsafe-inline'" : `'nonce-${nonce}'`};
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
.replace(/\s{2,}/g, ' ')
.trim();
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set('Content-Security-Policy', cspHeader);
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set('Content-Security-Policy', cspHeader);
return response;
}
This code additionally relaxes the CSP policy for the development environment (allowing 'unsafe-eval' and 'unsafe-inline'), without which React Fast Refresh would not function correctly locally.
Step 3: The _document.tsx File
This is where the server-side magic happens. We must read the token passed by the proxy in the x-nonce header and inject it into the <Head> and <NextScript> components. Additionally, we collect styles from styled-components using the ServerStyleSheet class.
Your src/pages/_document.tsx file should look like this:
// src/pages/_document.tsx
import Document, {
DocumentContext,
DocumentInitialProps,
Head,
Html,
Main,
NextScript,
} from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(
ctx: DocumentContext
): Promise<DocumentInitialProps & { nonce?: string }> {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
// Read the nonce from the custom header set by proxy
const nonce = ctx.req?.headers['x-nonce'] as string;
try {
// Wrap the app to collect styled-components styles during SSR
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
nonce,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}
render() {
const { nonce } = this.props;
return (
<Html lang="en">
<Head nonce={nonce} />
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
);
}
}
Remember to enable the styled-components compiler in your Next.js configuration file (next.config.ts):
// next.config.ts
const nextConfig: NextConfig = {
compiler: {
styledComponents: true,
},
};
Step 4: Client-side Bridge (Webpack Nonce)
This is the most subtle and technical part of the process. Why isn’t injecting the token into the HTML enough?
The problem is that when your application “comes alive” in the browser (the hydration process), the styled-components library takes control of the styles. With every state change or navigation, it may dynamically create new <style> tags. The browser will block them immediately if they do not have the current nonce token assigned.
The solution that has become a community standard (widely discussed in this GitHub thread) involves utilizing Webpack’s internal mechanism. Webpack has a special “magic” global variable called __webpack_nonce__. If it is set when the scripts run, Webpack (and subsequently styled-components) will automatically include its value in every dynamically created script or style element.
We create a file src/utils/csp-nonce.ts, which acts as a "bridge" — it extracts the token from a secure place in the DOM (where Next.js placed it) and passes it to the Webpack engine:
TypeScript
// src/utils/csp-nonce.ts
declare let __webpack_nonce__: string | undefined;
/**
* "Magic Nonce" mechanism for Webpack and styled-components.
* We leverage the fact that Next.js by default places the current nonce
* in the <noscript data-n-css="VALUE"> element.
*/
if (typeof document !== 'undefined') {
const cspNonce = document
.querySelector('noscript[data-n-css]')
?.getAttribute('data-n-css');
if (cspNonce) {
try {
// Assignment to Webpack's global variable
__webpack_nonce__ = cspNonce;
} catch (error) {
console.error('Error setting __webpack_nonce__:', error);
}
}
}
Why does this work?
According to the architecture described by the developers, styled-components version 6 (and newer) automatically looks for the __webpack_nonce__ variable if it is available in the global scope. Thanks to this, you don't have to manually pass the token to every component or theme — the library itself ensures that every new <style> tag complies with the CSP policy served by the HTTP headers.
Step 5: The Silent Killer — Import Order in _app.tsx
This is the most common cause of frustration. Your new csp-nonce.ts file must be imported first in the _app.tsx file, before importing styled-components.
If you import styled-components first, the library's internal function that checks for the token will return undefined, and your styles will be blocked in production without a clear error message.
Your src/pages/_app.tsx should look like this:
TypeScript
// src/pages/_app.tsx
// ⚠️ THIS MUST BE THE VERY FIRST IMPORT
import '@/utils/csp-nonce';
// Now it's safe to import styled-components and everything that depends on it
import type { AppProps } from 'next/app';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles, theme } from '@/shared/styles';
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider theme={theme}>
<GlobalStyles />
<Component {...pageProps} />
</ThemeProvider>
);
}t
It is worth leaving a clear comment in the code, as shown in the example above, so that no one accidentally (or via an automated formatter) re-sorts the imports in the future and breaks the CSP policy.
Building this workflow requires an understanding of individual rendering processes, but a correctly assembled chain of dependencies guarantees a robust XSS defense, regardless of the complexity of your styling mechanisms!
메타데이터
- post_id
- 0925d3dc1a7c
- slug
- content-security-policy-csp-in-next-js-and-styled-components-the-complete-guide-0925d3dc1a7c
- url
- https://medium.com/@kamil.witkowski0707/content-security-policy-csp-in-next-js-and-styled-components-the-complete-guide-0925d3dc1a7c
- canonical_url
- https://medium.com/@kamil.witkowski0707/content-security-policy-csp-in-next-js-and-styled-components-the-complete-guide-0925d3dc1a7c
- author_url
- https://medium.com/@kamil.witkowski0707
- status
- ok
- fetched_at
- 2026-06-24 04:09:36