Skip to main content

security-patterns

Next.js 16 security for production apps — security headers, nonce-based Content Security Policy in proxy.ts, CSRF and allowed origins for Server Actions, CORS for Route Handlers, rate limiting, server-only data access layers, environment variable safety, and keeping next patched against current security advisories. Use when hardening an app. Login and sessions belong to auth-patterns.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
November 1, 2026 at 02:46
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
security-patterns
description
Next.js 16 security for production apps — security headers, nonce-based Content Security Policy in proxy.ts, CSRF and allowed origins for Server Actions, CORS for Route Handlers, rate limiting, server-only data access layers, environment variable safety, and keeping next patched against current security advisories. Use when hardening an app. Login and sessions belong to auth-patterns.
# Security Patterns ## Environment Variable Safety Only variables prefixed with `NEXT_PUBLIC_` are exposed to the browser. All others are server-only. ```bash # .env.local DATABASE_URL="postgresql://..." # Server only — NEVER sent to browser API_SECRET="sk-..." # Server only NEXT_PUBLIC_APP_URL="https://myapp.com" # Available in browser code ``` **Prevent accidental server code in client bundles** with the `server-only` package: ```bash npm install server-only ``` ```typescript // lib/db.ts import 'server-only' // Build error if imported in a Client Component import { Pool } from 'pg' export const db = new Pool({ connectionString: process.env.DATABASE_URL }) ``` If a Client Component imports this file (directly or transitively), the build will fail with a clear error. ## Content Security Policy (CSP) Use `proxy.ts` (Next.js 16) to add CSP headers with a nonce: ```typescript // proxy.ts import { NextRequest, NextResponse } from 'next/server' export function proxy(request: NextRequest) { const nonce = Buffer.from(crypto.randomUUID()).toString('base64') const isDev = process.env.NODE_ENV === 'development' const cspHeader = ` default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ''}; style-src 'self' '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; ` const contentSecurityPolicyHeaderValue = cspHeader .replace(/\s{2,}/g, ' ') .trim() const requestHeaders = new Headers(request.headers) requestHeaders.set('x-nonce', nonce) requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue) const response = NextResponse.next({ request: { headers: requestHeaders }, }) response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue) return response } ``` Filter proxy to skip static assets: ```typescript // proxy.ts (add at the bottom) export const config = { matcher: [ { source: '/((?!api|_next/static|_next/image|favicon.ico).*)', missing: [ { type: 'header', key: 'next-router-prefetch' }, { type: 'header', key: 'purpose', value: 'prefetch' }, ], }, ], } ``` Read the nonce in a layout to pass it to Script components: ```tsx // app/layout.tsx import { headers } from 'next/headers' import Script from 'next/script' export default async function RootLayout({ children }: { children: React.ReactNode }) { const headersList = await headers() const nonce = headersList.get('x-nonce') ?? '' return ( <html lang="en"> <body> {children} <Script src="https://analytics.example.com/script.js" strategy="afterInteractive" nonce={nonce} /> </body> </html> ) } ``` > **Note:** In Next.js 16, the nonce is automatically applied to framework scripts — since 16.3.5 also to the `<script>` tags of `loading` and `template` files. You only need to set it manually on third-party `<Script>` components. ## Security Headers Add security headers via `next.config.js`: ```javascript // next.config.js const securityHeaders = [ { key: 'X-DNS-Prefetch-Control', value: 'on' }, { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, ] module.exports = { async headers() { return [ { source: '/(.*)', headers: securityHeaders, }, ] }, } ``` ## CSRF Protection **Server Actions** have built-in CSRF protection — Next.js validates the `Origin` header automatically. No additional setup needed. **Route Handlers** require manual CSRF protection for state-changing operations: ```typescript // app/api/data/route.ts import { headers } from 'next/headers' export async function POST(request: Request) { const headersList = await headers() const origin = headersList.get('origin') const host = headersList.get('host') // Verify origin matches host if (!origin || new URL(origin).host !== host) { return new Response('CSRF validation failed', { status: 403 }) } // Process request... } ``` ## CORS for Route Handlers ```typescript // app/api/data/route.ts const ALLOWED_ORIGINS = ['https://myapp.com', 'https://admin.myapp.com'] export async function GET(request: Request) { const origin = request.headers.get('origin') ?? '' const data = await getData() return Response.json(data, { headers: { 'Access-Control-Allow-Origin': ALLOWED_ORIGINS.includes(origin) ? origin : '', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }) } export async function OPTIONS() { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }) } ``` ## XSS Prevention - **Avoid `dangerouslySetInnerHTML`** — use it only for trusted content (e.g., sanitized CMS output) - If you must render HTML, sanitize first: ```typescript import DOMPurify from 'isomorphic-dompurify' export function RichContent({ html }: { html: string }) { const clean = DOMPurify.sanitize(html) return <div dangerouslySetInnerHTML={{ __html: clean }} /> } ``` - React automatically escapes string values in JSX — `{userInput}` is safe - Never construct HTML strings from user input in Server Components ## Input Validation in Server Actions Always validate and sanitize input on the server — never trust client-side validation alone: ```typescript 'use server' import { z } from 'zod' const CommentSchema = z.object({ content: z.string() .min(1, 'Comment cannot be empty') .max(1000, 'Comment too long') .transform(s => s.trim()), postId: z.string().uuid('Invalid post ID'), }) export async function addComment(formData: FormData) { const parsed = CommentSchema.safeParse({ content: formData.get('content'), postId: formData.get('postId'), }) if (!parsed.success) { return { errors: parsed.error.flatten().fieldErrors } } // Safe to use parsed.data — validated and typed await db.comment.create({ data: parsed.data }) } ``` ## Data Access Layer Centralize authorization checks — don't scatter them across routes: ```typescript // lib/dal.ts (Data Access Layer) import 'server-only' import { auth } from '@/lib/auth' import { redirect } from 'next/navigation' export async function getUser() { const session = await auth() if (!session?.user) { redirect('/login') } return session.user } export async function getUserPosts() { const user = await getUser() // Auth check built in return db.post.findMany({ where: { authorId: user.id } }) } ``` ## Keep Next.js Patched Since 2026-07-13 Next.js runs a formal security-release program (advance notice roughly monthly, ad-hoc patches for urgent issues). The 16.3 line has shipped critical fixes — unauthenticated RCE on Windows-hosted servers and via AVIF in Image Optimization (16.3.3), RCE in `next/og` `ImageResponse` (16.3.6) — and the September 2026 release (16.3.8) added a high-severity SSRF in Image Optimization, SSG/ISR cache-poisoning fixes, two `use cache` leaks (Draft Mode content, nested root params) and an origin check for the dev-server `/_next/mcp` endpoint. Pin `next@^16.3.8` (15.5.x users: 15.5.27 or later) and re-check each month: ```bash npm ls next && npm view next@latest version ``` Also narrow `images.remotePatterns` to the exact hostnames (and pathnames) you load — the 16.3.8 SSRF needed an allow-listed remote URL, and apps with no `remotePatterns` were not affected. ## Production Security Checklist - [ ] `next` on the latest patch (at least 16.3.8 on the 16.3 line, 15.5.27 on 15.5) - [ ] No secrets in `NEXT_PUBLIC_*` variables - [ ] `server-only` package on all server modules (db, auth, secrets) - [ ] CSP headers with nonces for inline scripts - [ ] Security headers (HSTS, X-Frame-Options, X-Content-Type-Options) - [ ] Zod validation on all Server Actions and Route Handlers - [ ] CSRF check on Route Handlers that mutate state - [ ] `HttpOnly`, `Secure`, `SameSite` flags on auth cookies - [ ] No `dangerouslySetInnerHTML` with unsanitized content - [ ] Rate limiting on auth endpoints and public APIs - [ ] `poweredByHeader: false` in `next.config.js` - [ ] `images.remotePatterns` limited to the hosts you actually load ## What This Skill Does NOT Cover - Authentication/authorization flows (see `auth-patterns`) - Database security (ORM-specific — use Prisma/Drizzle docs) - DDoS protection (infrastructure-level — use Vercel/Cloudflare) - Secrets management services (Vault, AWS Secrets Manager)
View on GitHub