docs: Improve Content Security Policy documentation (#80580)

# Improve Content Security Policy Documentation

This PR comprehensively improves the Next.js Content Security Policy
(CSP) documentation to address community feedback from Reddit and GitHub
discussions about gaps in the current documentation.

## Changes Made

### 1. Enhanced Nonce Flow Explanation
- Added detailed section explaining how Next.js automatically detects
and applies nonces during server-side rendering
- Clarified the 3-step process: middleware generation → Next.js
extraction → automatic application
- Explained that developers don't need to manually add nonces to every
script tag

### 2. Static vs Dynamic Rendering Documentation
- Added comprehensive section explaining performance implications of
using nonces
- Documented that nonces force all pages into dynamic rendering
- Explained trade-offs: slower page loads, increased server load, no CDN
caching
- Added guidance on when to use nonces vs alternatives

### 3. Experimental SRI (Subresource Integrity) Documentation
- Documented the experimental `sri.algorithm` configuration option
- Explained hash-based CSP as an alternative to nonces that preserves
static generation
- Included configuration examples and benefits/limitations
- Noted that SRI is webpack-only, App Router only, and experimental

### 4. Development vs Production Considerations
- Added section addressing environment-specific CSP requirements
- Documented need for `'unsafe-eval'` in development for HMR
- Provided conditional CSP examples based on NODE_ENV

### 5. Comprehensive Troubleshooting Section
- Added solutions for error pages that don't work with strict CSP
- Documented third-party script integration with CSP
- Included common CSP violation fixes
- Added examples for both App Router and Pages Router

### 6. Practical Examples
- Static site with hash-based CSP using SRI
- Dynamic site with nonce-based CSP
- Mixed approach for different page types
- Environment-aware CSP configuration

### 7. Enhanced Pages Router Documentation
- Added `_document.tsx` nonce access example
- Improved `getServerSideProps` examples
- Added third-party script integration examples

## Community Feedback Addressed

This PR directly addresses the Reddit thread feedback about:
- ❌ "it is not clear that the nonce is then picked up by the rest of the
pages" → ✅ Added detailed nonce flow explanation
- ❌ "it is not clear that the pages therefore are not statically
generated anymore" → ✅ Added static vs dynamic rendering section
- ❌ "The solution of being able to provide the sha256 hashes... is not
supported, and it is not mentioned in the documentation" → ✅ Added
experimental SRI documentation
- ❌ Poor documentation of strict CSP implementation → ✅ Added
comprehensive examples and troubleshooting

## Technical Details

- **File Modified**: `docs/01-app/02-guides/content-security-policy.mdx`
- **Lines Added**: 642 insertions
- **Maintains**: Existing documentation structure and Next.js
conventions
- **Supports**: Both App Router and Pages Router with appropriate
switcher components

## Testing

- Verified all code examples use correct Next.js APIs and patterns
- Ensured documentation follows existing Next.js docs structure
- Confirmed all referenced configuration options exist in the codebase
- Validated that experimental features are properly marked as such

---

**Link to Devin run**:
https://app.devin.ai/sessions/e7e5fa95dc0146b3ab79aac72c53ed13
**Requested by**: [email protected]

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: [email protected] <[email protected]>
Co-authored-by: Joseph Chamochumbi <[email protected]>
This commit is contained in:
devin-ai-integration[bot]
2025-08-04 18:47:26 +02:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> [email protected] <[email protected]> Joseph Chamochumbi
parent 92042340b3
commit 9d3069adc7
2 changed files with 428 additions and 4 deletions
@@ -27,7 +27,9 @@ A [nonce](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/nonce) i
### Why use a nonce?
Even though CSPs are designed to block malicious scripts, there are legitimate scenarios where inline scripts are necessary. In such cases, nonces offer a way to allow these scripts to execute if they have the correct nonce.
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.
### Adding a nonce with Middleware
@@ -170,6 +172,46 @@ export const config = {
}
```
### How nonces work in Next.js
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:
1. **Middleware generates a nonce**: Your middleware creates a unique nonce for the request, adds it to your `Content-Security-Policy` header, and also sets it in a custom `x-nonce` header.
2. **Next.js extracts the nonce**: During rendering, Next.js parses the `Content-Security-Policy` header and extracts the nonce using the `'nonce-{value}'` pattern.
3. **Nonce is applied automatically**: Next.js attaches the nonce to:
- Framework scripts (React, Next.js runtime)
- Page-specific JavaScript bundles
- Inline styles and scripts generated by Next.js
- Any `<Script>` components using the `nonce` prop
Because of this automatic behavior, you don’t need to manually add a nonce to each tag.
### Forcing dynamic rendering
If you're using nonces, you may need to explicitly opt pages into dynamic rendering:
```tsx filename="app/page.tsx" switcher
import { connection } from 'next/server'
export default async function Page() {
// wait for an incoming request to render this page
await connection()
// Your page content
}
```
```jsx filename="app/page.jsx" switcher
import { connection } from 'next/server'
export default async function Page() {
// wait for an incoming request to render this page
await connection()
// Your page content
}
```
### Reading the nonce
<PagesOnly>
@@ -215,6 +257,85 @@ export async function getServerSideProps({ req }) {
}
```
You can also access the nonce in `_document.tsx` for Pages Router applications:
```tsx filename="pages/_document.tsx" switcher
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
```
```jsx filename="pages/_document.jsx" switcher
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>
@@ -257,6 +378,38 @@ export default async function Page() {
</AppOnly>
## Static vs Dynamic Rendering with CSP
Using nonces has important implications for how your Next.js application renders:
### Dynamic Rendering Requirement
When you use nonces in your CSP, **all pages must be dynamically rendered**. This means:
- Pages will build successfully but may encounter runtime errors if not properly configured for dynamic rendering
- Each request generates a fresh page with a new nonce
- Static optimization and Incremental Static Regeneration (ISR) are disabled
- Pages cannot be cached by CDNs without additional configuration
- **Partial Prerendering (PPR) is incompatible** with nonce-based CSP since static shell scripts won't have access to the nonce
### Performance Implications
The shift from static to dynamic rendering affects performance:
- **Slower initial page loads**: Pages must be generated on each request
- **Increased server load**: Every request requires server-side rendering
- **No CDN caching**: Dynamic pages cannot be cached at the edge by default
- **Higher hosting costs**: More server resources needed for dynamic rendering
### When to use nonces
Consider nonces when:
- You have strict security requirements that prohibit `'unsafe-inline'`
- Your application handles sensitive data
- You need to allow specific inline scripts while blocking others
- Compliance requirements mandate strict CSP
## Without Nonces
For applications that do not require nonces, you can set the CSP header directly in your [`next.config.js`](/docs/app/api-reference/config/next-config-js) file:
@@ -292,6 +445,276 @@ module.exports = {
}
```
<AppOnly>
## Hash-based CSP with Subresource Integrity (Experimental)
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.
> **Warning**: This feature is experimental and only available with webpack bundler in App Router applications.
### How SRI works
Instead of using nonces, SRI generates cryptographic hashes of your JavaScript and CSS files at build time. These hashes are then used in your CSP policy to allow specific scripts and styles.
### Enabling SRI
Add the experimental SRI configuration to your `next.config.js`:
```js filename="next.config.js"
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
sri: {
algorithm: 'sha256', // or 'sha384' or 'sha512'
},
},
}
module.exports = nextConfig
```
### Using SRI with CSP
When SRI is enabled, you can use hash-based CSP policies:
```js filename="next.config.js"
const cspHeader = `
default-src 'self';
script-src 'self'; /* Next.js will automatically add hashes */
style-src 'self'; /* Next.js will automatically add hashes */
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, ''),
},
],
},
]
},
}
```
### Benefits of SRI over nonces
- **Static generation**: Pages can be statically generated and cached
- **CDN compatibility**: Static pages work with CDN caching
- **Better performance**: No server-side rendering required for each request
- **Build-time security**: Hashes are generated at build time, ensuring integrity
### Limitations of SRI
- **Experimental**: Feature may change or be removed
- **Webpack only**: Not available with Turbopack
- **App Router only**: Not supported in Pages Router
- **Build-time only**: Cannot handle dynamically generated scripts
</AppOnly>
## Development vs Production Considerations
CSP implementation differs between development and production environments:
### Development Environment
In development, you will need to enable `'unsafe-eval'` to support APIs that provide additional debugging information:
```ts filename="middleware.ts" switcher
export function middleware(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' 'nonce-${nonce}' ${isDev ? "'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;
`
// Rest of middleware implementation
}
```
```js filename="middleware.js" switcher
export function middleware(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' 'nonce-${nonce}' ${isDev ? "'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;
`
// Rest of middleware implementation
}
```
### Production Deployment
Common issues in production:
- **Nonce not applied**: Ensure your middleware runs on all necessary routes
- **Static assets blocked**: Verify your CSP allows Next.js static assets
- **Third-party scripts**: Add necessary domains to your CSP policy
## Troubleshooting
### Third-party Scripts
<AppOnly>
When using third-party scripts with CSP:
```tsx filename="app/layout.tsx" switcher
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>
)
}
```
```jsx filename="app/layout.jsx" switcher
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:
```tsx filename="pages/_app.tsx" switcher
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}
/>
</>
)
}
```
```jsx filename="pages/_app.jsx" switcher
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:
```ts filename="middleware.ts" switcher
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;
`
```
```js filename="middleware.js" switcher
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;
`
```
### Common CSP Violations
1. **Inline styles**: Use CSS-in-JS libraries that support nonces or move styles to external files
2. **Dynamic imports**: Ensure dynamic imports are allowed in your script-src policy
3. **WebAssembly**: Add `'wasm-unsafe-eval'` if using WebAssembly
4. **Service workers**: Add appropriate policies for service worker scripts
## Version History
We recommend using `v13.4.20+` of Next.js to properly handle and apply nonces.
| 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. |
+3 -2
View File
@@ -1,8 +1,9 @@
import { headers } from "next/headers";
import Script from "next/script";
export default function Page() {
const nonce = headers().get("x-nonce");
export default async function Page() {
const headerStore = await headers();
const nonce = headerStore.get("x-nonce");
return <Script src="https://..." strategy="afterInteractive" nonce={nonce} />;
}