• 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

or to participate.