ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β π nextjs/app/guides/content-security-policy β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
{/* The content of this doc is shared between the app and pages router. You can use the <PagesOnly>Content</PagesOnly> component to add content that is specific to the Pages Router. Any shared content should not be wrapped in a component. */}
Content Security Policy (CSP) is important to guard your Next.js application against various security threats such as cross-site scripting (XSS), clickjacking, and other code injection attacks.
By using CSP, developers can specify which origins are permissible for content sources, scripts, stylesheets, images, fonts, objects, media (audio, video), iframes, and more.
<details> <summary>Examples</summary> </details>A nonce is a unique, random string of characters created for a one-time use. It is used in conjunction with CSP to selectively allow certain inline scripts or styles to execute, bypassing strict CSP directives.
CSP can block both inline and external scripts to prevent attacks. A nonce lets you safely allow specific scripts to runβonly if they include the matching nonce value.
If an attacker wanted to load a script into your page, they'd need to guess the nonce value. That's why the nonce must be unpredictable and unique for every request.
Proxy enables you to add headers and generate nonces before the page renders.
Every time a page is viewed, a fresh nonce should be generated. This means that you must use dynamic rendering to add nonces.
For example:
import { NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' '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 newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, ' ')
.trim()
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
response.headers.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
return response
}
import { NextResponse } from 'next/server'
export function proxy(request) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' '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 newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader
.replace(/\s{2,}/g, ' ')
.trim()
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
requestHeaders.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
})
response.headers.set(
'Content-Security-Policy',
contentSecurityPolicyHeaderValue
)
return response
}
By default, Proxy runs on all requests. You can filter Proxy to run on specific paths using a matcher.
We recommend ignoring matching prefetches (from next/link) and static assets that don't need the CSP header.
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
}
To use a nonce, your page must be dynamically rendered. This is because Next.js applies nonces during server-side rendering, based on the CSP header present in the request. Static pages are generated at build time, when no request or response headers existβso no nonce can be injected.
Hereβs how nonce support works in a dynamically rendered page:
Content-Security-Policy header, and also sets it in a custom x-nonce header.Content-Security-Policy header and extracts the nonce using the 'nonce-{value}' pattern.<Script> components using the nonce propBecause of this automatic behavior, you donβt need to manually add a nonce to each tag.
If you're using nonces, you may need to explicitly opt pages into dynamic rendering:
import { connection } from 'next/server'
export default async function Page() {
// wait for an incoming request to render this page
await connection()
// Your page content
}
import { connection } from 'next/server'
export default async function Page() {
// wait for an incoming request to render this page
await connection()
// Your page content
}
import Script from 'next/script'
import type { GetServerSideProps } from 'next'
export default function Page({ nonce }) {
return (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const nonce = req.headers['x-nonce']
return { props: { nonce } }
}
import Script from 'next/script'
export default function Page({ nonce }) {
return (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
export async function getServerSideProps({ req }) {
const nonce = req.headers['x-nonce']
return { props: { nonce } }
}
You can also access the nonce in _document.tsx for Pages Router applications:
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext,
DocumentInitialProps,
} from 'next/document'
interface ExtendedDocumentProps extends DocumentInitialProps {
nonce?: string
}
class MyDocument extends Document<ExtendedDocumentProps> {
static async getInitialProps(
ctx: DocumentContext
): Promise<ExtendedDocumentProps> {
const initialProps = await Document.getInitialProps(ctx)
const nonce = ctx.req?.headers?.['x-nonce'] as string | undefined
return {
...initialProps,
nonce,
}
}
render() {
const { nonce } = this.props
return (
<Html lang="en">
<Head nonce={nonce} />
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
)
}
}
export default MyDocument
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
const nonce = ctx.req?.headers?.['x-nonce']
return {
...initialProps,
nonce,
}
}
render() {
const { nonce } = this.props
return (
<Html lang="en">
<Head nonce={nonce} />
<body>
<Main />
<NextScript nonce={nonce} />
</body>
</Html>
)
}
}
export default MyDocument
</PagesOnly>
<AppOnly>
You can read the nonce from a Server Component using headers:
import { headers } from 'next/headers'
import Script from 'next/script'
export default async function Page() {
const nonce = (await headers()).get('x-nonce')
return (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
import { headers } from 'next/headers'
import Script from 'next/script'
export default async function Page() {
const nonce = (await headers()).get('x-nonce')
return (
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
)
}
</AppOnly>
Using nonces has important implications for how your Next.js application renders:
When you use nonces in your CSP, all pages must be dynamically rendered. This means:
The shift from static to dynamic rendering affects performance:
Consider nonces when:
'unsafe-inline'For applications that do not require nonces, you can set the CSP header directly in your next.config.js file:
const cspHeader = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
]
},
}
<AppOnly>
As an alternative to nonces, Next.js offers experimental support for hash-based CSP using Subresource Integrity (SRI). This approach allows you to maintain static generation while still having a strict CSP.
Good to know: This feature is experimental and only available with webpack bundler in App Router applications.
Instead of using nonces, SRI generates cryptographic hashes of your JavaScript files at build time. These hashes are added as integrity attributes to script tags, allowing browsers to verify that files haven't been modified during transit.
Add the experimental SRI configuration to your next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
sri: {
algorithm: 'sha256', // or 'sha384' or 'sha512'
},
},
}
module.exports = nextConfig
When SRI is enabled, you can continue using your existing CSP policies. SRI works independently by adding integrity attributes to your assets:
Good to know: For dynamic rendering scenarios, you can still generate nonces with proxy if needed, combining both SRI integrity attributes and nonce-based CSP approaches.
const cspHeader = `
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
`
module.exports = {
experimental: {
sri: {
algorithm: 'sha256',
},
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader.replace(/\n/g, ''),
},
],
},
]
},
}
CSP implementation differs between development and production environments:
In development, you will need to enable 'unsafe-eval' to support APIs that provide additional debugging information:
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;
`
// Rest of proxy implementation
}
export function proxy(request) {
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;
`
// Rest of proxy implementation
}
Common issues in production:
When using third-party scripts with CSP:
import { GoogleTagManager } from '@next/third-parties/google'
import { headers } from 'next/headers'
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const nonce = (await headers()).get('x-nonce')
return (
<html lang="en">
<body>
{children}
<GoogleTagManager gtmId="GTM-XYZ" nonce={nonce} />
</body>
</html>
)
}
import { GoogleTagManager } from '@next/third-parties/google'
import { headers } from 'next/headers'
export default async function RootLayout({ children }) {
const nonce = (await headers()).get('x-nonce')
return (
<html lang="en">
<body>
{children}
<GoogleTagManager gtmId="GTM-XYZ" nonce={nonce} />
</body>
</html>
)
}
</AppOnly>
<PagesOnly>
When using third-party scripts with CSP, ensure you add the necessary domains and pass the nonce:
import type { AppProps } from 'next/app'
import Script from 'next/script'
export default function App({ Component, pageProps }: AppProps) {
const nonce = pageProps.nonce
return (
<>
<Component {...pageProps} />
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
</>
)
}
import Script from 'next/script'
export default function App({ Component, pageProps }) {
const nonce = pageProps.nonce
return (
<>
<Component {...pageProps} />
<Script
src="https://www.googletagmanager.com/gtag/js"
strategy="afterInteractive"
nonce={nonce}
/>
</>
)
}
</PagesOnly>
Update your CSP to allow third-party domains:
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://www.googletagmanager.com;
connect-src 'self' https://www.google-analytics.com;
img-src 'self' data: https://www.google-analytics.com;
`
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://www.googletagmanager.com;
connect-src 'self' https://www.google-analytics.com;
img-src 'self' data: https://www.google-analytics.com;
`
'wasm-unsafe-eval' if using WebAssembly| Version | Changes |
| ---------- | ------------------------------------------------------------- |
| v14.0.0 | Experimental SRI support added for hash-based CSP |
| v13.4.20 | Recommended for proper nonce handling and CSP header parsing. |
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ