Module A-13·25 min read

Nonce-based CSP in Middleware and the CDN cache invalidation trap (a cached nonce is not a nonce), SSRF via next/image remotePatterns wildcard misconfiguration, SSRF via Server Actions, secret leakage with server-only and taint API, and rate limiting: edge vs application layer.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

A-13 — Security Architecture

Who this is for: Architects who need to think about the attack surface of a Next.js application — not just "add a Content Security Policy" but understanding what's uniquely exposed by server-side rendering, what Server Actions change about the CSRF threat model, how secrets leak through the RSC boundary, and the layered defence strategy that holds up under real-world attack conditions.


The Next.js Attack Surface Map

Before defending, map what's exposed. A deployed Next.js application has a larger attack surface than a static site or a pure API server:

text

The unique risk in Next.js is the blurred server/client boundary. Code that looks like frontend code runs on the server. Data fetched for rendering might contain more than it should. The defences must address both.


Environment Variable Leakage

The most common security mistake in Next.js: accidentally including server-only secrets in the client bundle.

Any environment variable prefixed with NEXT_PUBLIC_ is baked into the client JavaScript bundle at build time. Anyone can extract it from the bundle. This is by design — it's for public API keys, not secrets.

The problem is that engineers forget and use process.env.DATABASE_URL or process.env.STRIPE_SECRET_KEY in a Client Component. TypeScript doesn't catch this. The variable is simply undefined on the client... unless the Next.js build includes it (it doesn't by default, but developer error is possible).

Three layers of defence:

Layer 1: server-only package

ts

Layer 2: taint API (from A-1 — marking values that must never reach the client):

ts

Layer 3: NEXT_PUBLIC_ discipline — audit any NEXT_PUBLIC_ variable before it's added. These are intentionally public. If someone adds NEXT_PUBLIC_DATABASE_URL, that's a critical security incident.

Automated check in CI:

bash

Content Security Policy

A Content Security Policy (CSP) is an HTTP response header that tells browsers which resources they're allowed to load. It's the primary defence against Cross-Site Scripting (XSS) — even if an attacker injects a <script> tag, the browser refuses to execute it if it's not from an allowed source.

Next.js's streaming architecture complicates CSP: the streaming runtime injects inline <script> tags to swap Suspense content. A strict CSP (script-src 'self') blocks these scripts and breaks the page.

The solution is nonce-based CSP:

ts
tsx

Pass the nonce to Next.js via the next.config.ts:

ts

The 'strict-dynamic' in script-src allows scripts loaded by already-trusted scripts (your app's own bundles loading their chunks), preventing the need to whitelist every CDN URL.


CSRF — The Server Action Story

Classic CSRF attacks work by tricking a user's browser into making a cross-origin request that includes their session cookies. For example: a malicious page that includes a form that POSTs to bank.com/transfer.

Server Actions have built-in CSRF protection through two mechanisms:

  1. SameSite cookies. Auth.js v5 and Next.js's own cookie handling default to SameSite=Lax or SameSite=Strict. Cross-origin form submissions don't include the session cookie, so the action can't authenticate.

  2. Origin header validation. Next.js checks the Origin header on Server Action requests and rejects those originating from different domains.

These protections cover the common CSRF vectors. Where you're still responsible:

  • Custom Route Handlers — Next.js provides no automatic CSRF protection for Route Handlers. If you have POST /api/transfer, add your own CSRF token validation.
  • SameSite=None cookies — required for third-party contexts (e.g., an iframe). These are vulnerable to CSRF by definition. Use explicit CSRF tokens.
  • Subdomain attacks — if evil.example.com is attacker-controlled and you use SameSite=Lax, subdomains can send requests that include cookies for example.com.

SQL Injection and Prisma

Prisma's query API is parameterised by design — user input passed as arguments to Prisma queries is always escaped. Standard Prisma usage is not vulnerable to SQL injection.

The vulnerability is in raw queries:

ts

The template literal syntax for $queryRaw is the safe version. It looks similar to string interpolation but Prisma intercepts the values and parameterises them properly.


Security Headers — The Baseline

Every Next.js production application should set these response headers:

ts

What each header does:

  • HSTS — tells browsers to always use HTTPS for this domain, even if the user types HTTP
  • X-Frame-Options: DENY — prevents clickjacking (your page in an iframe)
  • X-Content-Type-Options: nosniff — prevents MIME-type sniffing attacks
  • Referrer-Policy — controls what's sent in the Referer header (prevents leaking URLs)
  • Permissions-Policy — denies access to browser APIs your app doesn't need

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.