- Tejaya's Blog
- Posts
- 🛡️ Secure Your Next.js 14+ App with Content Security Policy
🛡️ Secure Your Next.js 14+ App with Content Security Policy
Protect your blog from XSS attacks using nonce-based Content Security Policy headers (CSP).
Modern JavaScript apps are vulnerable to Cross-Site Scripting (XSS) attacks — and even your blog isn’t safe. One injected script can compromise your user’s trust, steal data, or inject malicious UIs.
The solution?
A strong Content Security Policy (CSP) with nonce-based script protection.
In this step-by-step guide, you’ll learn how to:
Set up CSP headers in a Next.js 14+ app using middleware
Dynamically generate nonces for inline scripts
Make it work across all rendering strategies:
✅ Static Site Generation (SSG)
✅ Server-Side Rendering (SSR)
✅ Incremental Static Regeneration (ISR)
We’ll be using the App Router + TypeScript — and by the end, your blog will be safer, smarter, and production-ready.
🔧 Step 1: Project Setup (Next.js 14+ with App Router)
Make sure you’re on Next.js v14 or higher and select the App Router during setup.
Use this command to bootstrap the project:
npx create-next-app@latest nextjs-csp-blog --app --typescript cd nextjs-csp-blog
Choose:
✅ App Router
✅ TypeScript
✅ Tailwind (optional)
🗂️ Step 2: File Structure Overview
Your folder structure should look like this:
/app
layout.tsx ← Uses CSP nonce
/blog
page.tsx ← Blog page (SSG/SSR/ISR)
/lib
csp.ts ← Helper to access nonce
/middleware.ts ← Injects CSP headers
We’ll walk through each of these step-by-step.
🔐 Step 3: Create Middleware to Inject CSP Headers
Create a new file at the project root:
middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import crypto from 'crypto'
export function middleware(request: NextRequest) {
const nonce = crypto.randomBytes(16).toString('base64')
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
frame-src 'none';
base-uri 'self';
`.replace(/\s{2,}/g, ' ').trim()
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
const response = NextResponse.next({
request: { headers: requestHeaders }
})
response.headers.set('Content-Security-Policy', csp)
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
This middleware does two things:
✅ Generates a new
nonce
for each request✅ Attaches CSP headers with that nonce
🔁 Step 4: Access Nonce on the Server
Create a utility to access the nonce value on the server using the new headers()
API.
lib/csp.ts
import { headers } from 'next/headers'
export function getCSPNonce(): string {
return headers().get('x-nonce') || ''
}
This allows you to read the nonce in layout and inject it into your script tags.
🎨 Step 5: Inject Nonce into Inline Scripts
Now modify your layout to use the CSP nonce.
app/layout.tsx
import './globals.css'
import { getCSPNonce } from '@/lib/csp'
export default function RootLayout({ children }: { children: React.ReactNode }) {
const nonce = getCSPNonce()
return (
<html lang="en">
<head>
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `console.log("✅CSP inline script executed")`,
}}
/>
</head>
<body>{children}</body>
</html>
)
}
✅ This ensures that only your inline scripts run — and any injected scripts will be blocked by the browser.
✍️ Step 6: Blog Page (SSG / SSR / ISR Ready)
Let’s now create a blog page that supports all rendering modes.
app/blog/page.tsx
export const dynamic = 'force-static' // Change to 'force-dynamic' for SSR
export const revalidate = 60 // Enable ISR
export default async function BlogPage() {
return (
<main className="p-8">
<h1 className="text-4xl font-bold">📝 CSP Secured Blog</h1>
<p>This page is protected by a strict Content Security Policy header.</p>
</main>
)
}
⚙️ What do these configs mean?
Rendering Mode | Flag | Description |
---|---|---|
SSG | force-static | Built once at compile time |
SSR | force-dynamic | Renders on every request |
ISR | revalidate + auto | Caches and re-renders periodically |
🧪 Step 7: Test Your Setup
Run the dev server:
npm run dev
Then open the browser → DevTools → Network tab → Inspect headers.
You should see:
✅
Content-Security-Policy
header✅
x-nonce
included✅ Your inline script logs to the console
Try pasting this into the browser console:
var s = document.createElement('script');
s.innerText = 'alert("XSS blocked!")';
document.body.appendChild(s);
🚫 It will be blocked because the nonce is missing. Success!
📬 Final Summary
✅ You’ve now implemented nonce-based CSP in your blog using:
✅ Next.js 14+ App Router
✅ TypeScript
✅ Dynamic CSP headers via middleware
✅ Nonce-based inline script protection
✅ Compatibility with SSG, SSR, and ISR
This setup ensures your content is protected against most common script injection attacks.
📥 Want More Secure App Guides?
Get more deep-dives like this in your inbox every week.
We cover full-stack security, SaaS development, and performance tuning — with real-world code, 🔒 Subscribe to Tejaya.Tech
Reply