|$ curl https://forge-ai.dev/api/markdown?path=docs/security/csp
$cat docs/content-security-policy.md
updated Recently·30 min read·published
Content Security Policy
Introduction
Content Security Policy (CSP) is an HTTP header that restricts which resources a browser is allowed to load. It's the primary defense against Cross-Site Scripting (XSS), clickjacking, and data injection attacks. A well-configured CSP eliminates entire classes of vulnerabilities.
CSP Directives
| Directive | Controls | Example |
|---|---|---|
| default-src | Fallback for all resource types | 'self' |
| script-src | JavaScript execution | 'self' 'nonce-abc' |
| style-src | CSS stylesheets | 'self' 'unsafe-inline' |
| img-src | Images | 'self' data: https: |
| connect-src | Fetch/XHR/WebSocket | 'self' api.example.com |
| frame-ancestors | Who can embed this page | 'none' |
Nonce-Based CSP
A nonce (number used once) is a random token generated per request. Only scripts with the correct nonce can execute, blocking any injected scripts.
middleware.ts
TypeScript
| 1 | // Next.js middleware — generate nonce per request |
| 2 | import { NextResponse } from "next/server"; |
| 3 | import type { NextRequest } from "next/server"; |
| 4 | import crypto from "crypto"; |
| 5 | |
| 6 | export function middleware(request: NextRequest) { |
| 7 | const nonce = crypto.randomBytes(16).toString("base64"); |
| 8 | |
| 9 | const csp = [ |
| 10 | "default-src 'self'", |
| 11 | "script-src 'self' 'nonce-" + nonce + "'", |
| 12 | "style-src 'self' 'nonce-" + nonce + "'", |
| 13 | "img-src 'self' data: https:", |
| 14 | "font-src 'self'", |
| 15 | "connect-src 'self' https://api.example.com", |
| 16 | "frame-ancestors 'none'", |
| 17 | "base-uri 'self'", |
| 18 | "form-action 'self'", |
| 19 | "report-uri /api/csp-report", |
| 20 | ].join("; "); |
| 21 | |
| 22 | const response = NextResponse.next(); |
| 23 | response.headers.set("Content-Security-Policy", csp); |
| 24 | response.headers.set("X-Content-Type-Options", "nosniff"); |
| 25 | response.headers.set("X-Frame-Options", "DENY"); |
| 26 | response.headers.set("X-XSS-Protection", "0"); |
| 27 | |
| 28 | // Pass nonce to the page via header |
| 29 | response.headers.set("X-Nonce", nonce); |
| 30 | |
| 31 | return response; |
| 32 | } |
| 33 | |
| 34 | export const config = { |
| 35 | matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], |
| 36 | }; |
layout.tsx
TypeScript
| 1 | // Use nonce in your layout |
| 2 | export default function RootLayout({ children, nonce }: { children: React.ReactNode; nonce: string }) { |
| 3 | return ( |
| 4 | <html lang="en"> |
| 5 | <head> |
| 6 | <script src="/analytics.js" nonce={nonce} /> |
| 7 | <style nonce={nonce}>{`.container { max-width: 1200px; }`}</style> |
| 8 | </head> |
| 9 | <body>{children}</body> |
| 10 | </html> |
| 11 | ); |
| 12 | } |
⚠
warning
Never use 'unsafe-inline' in production CSP. It completely defeats the purpose of CSP against XSS. Always use nonces or hashes.
CSP Reporting
csp-report/route.ts
TypeScript
| 1 | // CSP report endpoint (receives violation reports) |
| 2 | import { NextRequest, NextResponse } from "next/server"; |
| 3 | |
| 4 | export async function POST(request: NextRequest) { |
| 5 | const report = await request.json(); |
| 6 | |
| 7 | console.error("CSP Violation:", { |
| 8 | documentUri: report["csp-report"]["document-uri"], |
| 9 | violatedDirective: report["csp-report"]["violated-directive"], |
| 10 | blockedUri: report["csp-report"]["blocked-uri"], |
| 11 | sourceFile: report["csp-report"]["source-file"], |
| 12 | lineNumber: report["csp-report"]["line-number"], |
| 13 | }); |
| 14 | |
| 15 | // Send to monitoring (Datadog, Sentry, etc.) |
| 16 | await sendToMonitoring(report); |
| 17 | |
| 18 | return NextResponse.json({ status: "received" }); |
| 19 | } |
report-only.ts
TypeScript
| 1 | // Report-Only mode: test CSP without enforcing |
| 2 | const cspReportOnly = [ |
| 3 | "default-src 'self'", |
| 4 | "script-src 'self' 'nonce-${nonce}'", |
| 5 | "report-uri /api/csp-report", |
| 6 | "report-to csp-endpoint", |
| 7 | ].join("; "); |
| 8 | |
| 9 | // Send as Report-Only header first |
| 10 | response.headers.set("Content-Security-Policy-Report-Only", cspReportOnly); |
ℹ
info
Always deploy CSP in Report-Only mode first. Monitor violation reports for a few days, fix any legitimate resources being blocked, then switch to enforcement mode.
$Blueprint — Engineering Documentation·Section ID: SEC-CSP-01·Revision: 1.0