CSP prevents XSS attacks by controlling which resources the browser loads. Here is how to set it up in Next.js.
Middleware-Based CSP With Nonces
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Generate a unique nonce for each request
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
// Build CSP directives
const cspDirectives = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' blob: data: https:`,
`font-src 'self'`,
`connect-src 'self' https://vitals.vercel-insights.com`,
`frame-src 'self'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
`upgrade-insecure-requests`,
];
const cspHeader = cspDirectives.join("; ");
// Pass nonce to the app via request headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-nonce", nonce);
requestHeaders.set("Content-Security-Policy", cspHeader);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
// Set CSP on the response
response.headers.set("Content-Security-Policy", cspHeader);
return response;
}
export const config = {
matcher: [
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
],
};
Root Layout With Nonce
// app/layout.tsx
import { headers } from "next/headers";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const headerList = await headers();
const nonce = headerList.get("x-nonce") ?? "";
return (
<html lang="en">
<body>
<NonceProvider nonce={nonce}>
{children}
</NonceProvider>
</body>
</html>
);
}
Nonce Provider
// components/NonceProvider.tsx
"use client";
import { createContext, useContext } from "react";
const NonceContext = createContext<string>("");
export function NonceProvider({
nonce,
children,
}: {
nonce: string;
children: React.ReactNode;
}) {
return <NonceContext.Provider value={nonce}>{children}</NonceContext.Provider>;
}
export function useNonce() {
return useContext(NonceContext);
}
Script Component With Nonce
"use client";
import Script from "next/script";
import { useNonce } from "./NonceProvider";
export function Analytics() {
const nonce = useNonce();
return (
<Script
nonce={nonce}
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXX');
`,
}}
/>
);
}
CSP Violation Reporting
// app/api/csp-report/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body = await request.json();
const report = body["csp-report"] ?? body;
// Log violation for monitoring
console.warn("CSP Violation:", {
blockedUri: report["blocked-uri"],
violatedDirective: report["violated-directive"],
documentUri: report["document-uri"],
sourceFile: report["source-file"],
lineNumber: report["line-number"],
});
// In production, send to your logging service:
// await logToService("csp-violation", report);
return NextResponse.json({ received: true });
}
Add the reporting directive to your CSP:
const cspDirectives = [
// ... existing directives
`report-uri /api/csp-report`,
`report-to csp-endpoint`,
];
Report-Only Mode for Testing
Use Content-Security-Policy-Report-Only first to test without blocking:
// Use report-only during development
const headerName =
process.env.NODE_ENV === "development"
? "Content-Security-Policy-Report-Only"
: "Content-Security-Policy";
response.headers.set(headerName, cspHeader);
Additional Security Headers
// Add alongside CSP in middleware
response.headers.set(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload",
);
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("X-XSS-Protection", "0"); // Rely on CSP instead
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
response.headers.set(
"Permissions-Policy",
"camera=(), microphone=(), geolocation=()",
);
next.config.ts Alternative
For simpler CSP without nonces:
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data:",
"font-src 'self'",
"object-src 'none'",
"frame-ancestors 'none'",
].join("; "),
},
],
},
];
},
};
export default config;
Need Security Hardening?
We implement comprehensive security for web applications. Contact us to protect your site.