Next.js 16 XSS Hardening [2026] Security Cheat Sheet
Bottom Line
In Next.js 16, strict XSS defense is mostly about choosing the right CSP model, shrinking your server-to-client data surface, and treating every HTML sink and Server Action as hostile by default. Use nonces when policy strictness matters more than caching, and prefer experimental SRI when you need static generation.
Key Takeaways
- ›Next.js 16 renamed Middleware to Proxy; the runtime behavior is the same.
- ›Nonce-based CSP forces dynamic rendering, disables ISR, and complicates CDN caching.
- ›Experimental SRI keeps static generation and works with a strict hash-based CSP path.
- ›Server Actions are public HTTP endpoints; validate input and enforce auth on every mutation.
- ›Trusted Types became MDN Baseline 2026 on latest browsers, making DOM XSS sinks easier to lock down.
Next.js 16 gives you better defaults than older React stacks, but advanced XSS hardening still fails in familiar places: raw HTML rendering, loose CSPs, unsafe Server Actions, and over-broad data passed across the server-client boundary. This reference is optimized for fast implementation on April 27, 2026: pick a CSP strategy, audit the dangerous sinks, wire the right headers, and keep the fixes searchable for the rest of your team.
XSS Surface Map
Bottom Line
If you only do three things, do these: ship a strict CSP, eliminate or sanitize every HTML sink, and lock down Server Actions as if they were public APIs. In Next.js 16, the main architecture decision is nonce-based CSP versus experimental SRI.
What changed in 2026
- Next.js 16.2.2 is the current docs version referenced here.
- Starting with Next.js 16, Middleware is now called Proxy.
- Nonce-based CSP in Next.js requires dynamic rendering, which disables static optimization and ISR.
- Experimental SRI in App Router offers a static-friendly path for strict script integrity.
- MDN marks Trusted Types and require-trusted-types-for as Baseline 2026 on the latest browsers.
Highest-risk sinks in real Next.js apps
- dangerouslySetInnerHTML with user-controlled or parser-generated HTML.
- Markdown, MDX, CMS, comments, rich text, or email content rendered as HTML.
- Client-side URL injection into
href,src, or redirect flows. - Inline JSON or config blobs rendered into
<script>tags. - Legacy browser APIs such as
innerHTML,outerHTML,insertAdjacentHTML,eval(), andnew Function(). - Server Components passing broad objects into Client Components instead of narrow DTOs.
CSP and Security Headers
Use a strict nonce policy when runtime script control matters most
Choose this when the application handles sensitive data, compliance pushes you away from 'unsafe-inline', and you can afford request-time rendering.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const isDev = process.env.NODE_ENV === 'development'
const csp = [
"default-src 'self'",
"script-src 'self' 'nonce-" + nonce + "' 'strict-dynamic'" + (isDev ? " 'unsafe-eval'" : ''),
"style-src 'self'" + (isDev ? " 'unsafe-inline'" : " 'nonce-" + nonce + "'"),
"img-src 'self' blob: data:",
"font-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests"
].join('; ')
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).*)']
}
- Read the nonce in a Server Component with
await headers()and pass it tonext/scriptor supported third-party components. - Use development-only
'unsafe-eval'because Next.js docs note React debugging still depends on it in dev. - Force dynamic rendering with
await connection()on pages that need request-time nonce generation.
Use experimental SRI when you need static generation
Choose this when CDN caching, prerendering, and lower request-time cost matter more than per-request nonces.
import type { NextConfig } from 'next'
const isDev = process.env.NODE_ENV === 'development'
const cspHeader = [
"default-src 'self'",
"script-src 'self'" + (isDev ? " 'unsafe-eval'" : ''),
"style-src 'self'",
"img-src 'self' blob: data:",
"font-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
"upgrade-insecure-requests"
].join('; ')
const nextConfig: NextConfig = {
experimental: {
sri: {
algorithm: 'sha256'
}
},
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: cspHeader
}
]
}
]
}
}
export default nextConfig
- SRI is still experimental and currently documented for App Router.
- It works well for build-time assets, but it is not the answer for dynamically generated scripts.
- Inference: treat SRI as the performance-first option and nonces as the policy-first option.
Baseline headers worth shipping everywhere
const securityHeaders = [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }
]
- Prefer
frame-ancestorsin CSP for clickjacking protection; keepX-Frame-Optionsonly if you still care about older browser behavior. nosniffmatters if your product lets users upload or download files.
Searchable Audit Commands
Use the filter below to narrow the command list live. The snippet also wires lightweight keyboard support so the reference stays useful during incident work.
| Shortcut | Action | Why it helps |
|---|---|---|
/ | Focus filter | Fast jump to audit commands without reaching for the mouse. |
Ctrl+K / Cmd+K | Focus filter | Matches common app search muscle memory. |
Esc | Clear filter | Reset the list during triage. |
Inventory dangerous sinks
rg -n "dangerouslySetInnerHTML|innerHTML|outerHTML|insertAdjacentHTML|document.write\(|eval\(|new Function\(" app src components lib
- Start here before rewriting CSP. Most XSS fixes fail because teams never map the sinks.
Review server-client boundaries
rg -n "'use client'|'use server'|server-only|NEXT_PUBLIC_" app src lib
- Look for secret-bearing modules imported too close to Client Components.
Confirm framework and React versions
npm ls next react react-dom
- Useful when a repo upgrade left mixed assumptions around Proxy, CSP, or experimental features.
Verify response headers
curl -sI https://your-app.example | grep -Ei 'content-security-policy|x-content-type-options|referrer-policy|permissions-policy|strict-transport-security'
- Run this against production, preview, and any CDN front door, not just localhost.
Find CSP reporting and logging paths
rg -n "Content-Security-Policy|report-uri|report-to|csp-report|SecurityPolicyViolationEvent" app src lib
- Before sharing payloads, cookies, or violation samples in tickets, redact them with the Data Masking Tool.
document.addEventListener('DOMContentLoaded', function () {
var root = document.getElementById('cmd-ref')
if (!root) return
var input = document.getElementById('cmd-filter')
var items = Array.from(root.querySelectorAll('[data-filter-item]'))
function applyFilter() {
var q = (input.value || '').trim().toLowerCase()
items.forEach(function (item) {
var haystack = ((item.getAttribute('data-label') || '') + ' ' + (item.textContent || '')).toLowerCase()
item.hidden = q !== '' && !haystack.includes(q)
})
}
input.addEventListener('input', applyFilter)
document.addEventListener('keydown', function (event) {
var tag = document.activeElement && document.activeElement.tagName
var combo = event.key.toLowerCase() === 'k' && (event.ctrlKey || event.metaKey)
if ((event.key === '/' || combo) && tag !== 'INPUT' && tag !== 'TEXTAREA') {
event.preventDefault()
input.focus()
input.select()
}
if (event.key === 'Escape' && document.activeElement === input) {
input.value = ''
applyFilter()
input.blur()
}
})
})
Advanced Next.js 16 Usage
Lock down Server Actions and Route Handlers
- Next.js documents every exported Server Action as a public HTTP endpoint.
- Validate input on the server, not in the client form only.
- Check authentication and authorization inside the action itself.
- Use serverActions.allowedOrigins if reverse proxies or layered backends change the apparent host.
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com']
}
}
}
export default nextConfig
'use server'
import { verifySession } from '@/app/lib/dal'
export async function rotateApiKey(formData: FormData) {
const session = await verifySession()
if (!session || session.user.role !== 'admin') {
throw new Error('Forbidden')
}
const projectId = formData.get('projectId')
if (typeof projectId !== 'string' || projectId.length === 0) {
throw new Error('Invalid projectId')
}
// perform the mutation
}
Use DTOs and server-only modules to reduce accidental exposure
- Return only the fields a Client Component actually needs.
- Keep secret access in a dedicated Data Access Layer.
- Mark private modules with
server-onlyso client imports fail at build time. - Inference: this is one of the cheapest ways to eliminate XSS-adjacent data exposure before it reaches a sink.
import 'server-only'
export async function getProfileDTO(userId: string) {
const user = await db.user.findUnique({ where: { id: userId } })
return {
id: user.id,
displayName: user.displayName,
avatarUrl: user.avatarUrl
}
}
Add Trusted Types after CSP, not instead of CSP
Trusted Types help with DOM XSS sinks such as innerHTML. They are a strong progressive control for modern browsers, but they do not replace sanitization, DTO discipline, or server-side authorization.
const csp = [
"default-src 'self'",
"script-src 'self'",
"require-trusted-types-for 'script'",
"trusted-types app-html"
].join('; ')
Rollout Checklist
Deploy in this order
- Map all HTML and script sinks with search before touching headers.
- Pick nonce CSP or experimental SRI based on rendering and caching constraints.
- Ship Content-Security-Policy, nosniff, Referrer-Policy, and Permissions-Policy.
- Refactor broad server objects into DTOs and mark private modules with
server-only. - Re-audit every Server Action and Route Handler for authz, input validation, and origin assumptions.
- Add Trusted Types where DOM sink exposure still exists in modern-browser traffic.
Common breakages to expect
- Inline styles blocked unless your styling path supports the nonce model you chose.
- Third-party analytics or tag managers failing until their domains are explicitly allowed.
- WebAssembly paths requiring
'wasm-unsafe-eval'if you use WASM in the browser. - Static assets or image paths blocked because the final CDN origin was not represented in policy.
- Teams enabling React taint APIs in production even though Next.js documents them as experimental and not recommended for production.
Primary references
Frequently Asked Questions
How do I add CSP nonces in Next.js 16 without breaking static rendering? +
Are Next.js Server Actions safe against CSRF by default? +
POST for Server Actions and compares Origin with Host or X-Forwarded-Host, which blocks many CSRF cases, but you still need per-action auth and input validation.Is dangerouslySetInnerHTML ever safe in a Next.js app? +
dangerouslySetInnerHTML is trivial XSS. If you must support rich HTML, isolate the sanitizer and keep the output surface small.Should I enable React taint APIs in production for XSS protection? +
experimental.taint as experimental and not recommended for production, and it also warns that tainting should not be your only mechanism for preventing client exposure. Use DTOs, server-only, CSP, and authorization first.Get Engineering Deep-Dives in Your Inbox
Weekly breakdowns of architecture, security, and developer tooling — no fluff.