- 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-blogChoose:
β 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
noncefor 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 devThen open the browser β DevTools β Network tab β Inspect headers.
You should see:
β
Content-Security-Policyheaderβ
x-nonceincludedβ 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