Security Hardening & Version Migration
Security and version migration are the two lines of defense for production-level applications—the former prevents external attacks, while the latter ensures the tech stack remains debt-free.
1. What You'll Learn
- Implement XSS protection, CSRF tokens, and HTTP security headers (next.config.js headers)
- Prevent environment variable leaks (NEXT_PUBLIC_* convention / .env*.local in .gitignore)
- Use npm audit and Snyk to perform dependency security audits
- Master the v14→v15 migration: Asynchronous handling of params, cookies, and headers
- Understanding the v15→v16 Migration: proxy.ts Replacing Middleware, Turbopack Defaults, and "use cache" Migration
2. A True Story of a Security Engineer
(1) Pain Point: XSS attacks lead to user data leaks
Three months after the Bob team’s TaskFlow SaaS platform launched in Brazil, they received a security report:
"The attacker injected the
<script>tag into the 'Project Name' field and successfully stole session cookies from over 200 users. The investigation revealed that the 'Project Name' field on the Dashboard page was not properly escaped during output."
The security audit identified a series of issues:
| Vulnerability | Impact | Severity |
|---|---|---|
| XSS (Reflected) | Session hijacking, data theft | 🔴 Severe |
| Missing CSRF Token | Cross-Site Request Forgery | 🟠 High Risk |
| Missing HTTP Security Headers | Clickjacking, MIME sniffing | 🟡 Medium |
| NEXT_PUBLIC_ Abuse | API key exposed in JS bundle | 🔴 Severe |
| Dependency Vulnerability | lodash—known CVE, not patched | 🟠 High |
At the same time, technical debt is accumulating—the project is based on Next.js 14, but Next.js 16 was released six months ago, and the team is concerned about the security risks and performance losses associated with running an outdated version.
(2) Solution for Security Hardening + Version Migration
Bob approaches security in two steps:
# Step 1: Fix Security Vulnerabilities Immediately
npm audit fix
# Add a security header to next.config.js
# Add CSRF Token Go to All Forms
# Step 2: Version Migration (14→15→16)
npm install next@16 react@19 react-dom@19
(3) Revenue
| Dimension | Before Reinforcement | After Reinforcement |
|---|---|---|
| Security Score (Mozilla Observatory) | D (45/100) | A+ (100/100) |
| Known CVEs | 12 | 0 |
| Version | Next.js 14.2 | Next.js 16.2 LTS |
| Build Time | 45s | 12s (Turbopack) |
| Performance (Lighthouse) | 72 | 92 |
3. Configuring HTTP Security Headers
The next.config.js function in headers can inject HTTP response headers, overriding security policies such as CSP, HSTS, and X-Frame-Options.
graph TB
A[User Request] --> B[Next.js Applications]
B --> C[headers Function Handling]
C --> D[Inject the safety head]
D --> E[Browser Security Policy Enabled]
E --> F[CSP Block XSS]
E --> G[HSTS Mandatory HTTPS]
E --> H[X-Frame-Options Prevent Click Hijacking]
style C fill:#cce5ff
style E fill:#d4edda
| Safety Header | Purpose | Example Value |
|---|---|---|
Content-Security-Policy |
Prevent XSS and Data Injection | default-src 'self' |
Strict-Transport-Security |
Forced HTTPS | max-age=31536000; includeSubDomains |
X-Frame-Options |
Click Hijacking Prevention | SAMEORIGIN |
X-Content-Type-Options |
Prevent MIME sniffing | nosniff |
Referrer-Policy |
Controlling Referrer Information | strict-origin-when-cross-origin |
Permissions-Policy |
Control Browser API Permissions | camera=(), microphone=() |
(1) next.config.js Security Header Configuration
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
async headers() {
return [
{
// Apply to all routes
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://app.posthog.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data: https://*.taskflow.io https://avatars.githubusercontent.com",
"font-src 'self' data:",
"connect-src 'self' https://api.taskflow.io https://app.posthog.com https://o450000000.ingest.sentry.io",
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'",
"object-src 'none'"
].join('; ')
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: [
'camera=()',
'microphone=()',
'geolocation=()',
'interest-cohort=()'
].join(', ')
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
}
]
},
{
// API Additional Security Restrictions on Routing
source: '/api/(.*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Cache-Control',
value: 'no-store, no-cache, must-revalidate'
}
]
}
]
}
}
module.exports = nextConfig
(2) CSRF Token Protection
// src/components/Form.tsx
'use client'
import { useFormState } from 'react-dom'
import { createTask } from '@/actions/task'
export function TaskForm() {
const [state, formAction] = useFormState(createTask, { error: null })
return (
<form action={formAction}>
{/* CSRF Token Hidden Fields */}
<input type="hidden" name="csrf_token" value={getCsrfToken()} />
<input
type="text"
name="title"
placeholder="Task title"
className="border p-2 rounded"
/>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded"
>
Create Task
</button>
{state.error && (
<p className="text-red-600 mt-2">{state.error}</p>
)}
</form>
)
}
▶ Example: Header Validation
# Verify Security Header
curl -I https://taskflow.io
# Expected Output
HTTP/2 200
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-eval'...
strict-transport-security: max-age=31536000; includeSubDomains
x-frame-options: DENY
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
Output:
{"status":"ok","data":{}}
4. Preventing Environment Variable Leaks
Variables with the NEXT_PUBLIC_ prefix will be compiled into the client-side JS bundle—any secrets placed under this prefix will be exposed to the browser.
graph TB
A[.env.local] --> B{Environment Variable Prefix}
B -->|NEXT_PUBLIC_*| C[Inline to JS bundle]
B -->|Other| D[Server-side only]
C --> E[Browser DevTools As can be seen]
D --> F[Safety]
style C fill:#f8d7da
style D fill:#d4edda
style E fill:#f8d7da
| Variable Prefix | Scope of Use | Security Risks | Applicable Scenarios |
|---|---|---|---|
NEXT_PUBLIC_ |
Client + Server | 🔴 High (exposed in JS) | API public key, project name |
| No prefix | Server-only | 🟢 Secure | Database passwords, API keys |
process.env.* |
Injected during build | 🟡 Medium (depending on the build environment) | Configured during build |
(1) Environment Variable Security Checklist
# ✅ Safety:Using Variables Without Prefixes
DATABASE_URL=postgresql://user:pass@host:5432/db
AUTH_SECRET=super-secret-key-here
STRIPE_SECRET_KEY=sk_live_xxx
SENTRY_DSN=https://xxx@sentry.io/123
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
# ✅ Correct:NEXT_PUBLIC_ For public keys
NEXT_PUBLIC_POSTHOG_KEY=phc_publicKey
NEXT_PUBLIC_SENTRY_DSN=https://public@sentry.io/123
NEXT_PUBLIC_APP_URL=https://taskflow.io
# ❌ Danger: Do not store keys under NEXT_PUBLIC_
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxx # Exposed in the browser!
NEXT_PUBLIC_DATABASE_URL=postgresql://... # Exposed Database Connections!
(2) .gitignore Configuration
# .gitignore — Environment Variable Security
.env
.env*.local
.env.development.local
.env.test.local
.env.production.local
# Don't overlook .env.example(This is a template file)
!.env.example
(3) Environment Variable Verification Script
// src/lib/env-validation.ts
// Ensure that sensitive environment variables are inaccessible on the client side
function validateEnv() {
if (typeof window !== 'undefined') {
const nextPublicVars = Object.keys(process.env)
.filter(key => key.startsWith('NEXT_PUBLIC_'))
// Check for accidental exposure of private variables
const sensitiveSuffixes = ['SECRET', 'KEY', 'PASSWORD', 'TOKEN', 'PRIVATE']
for (const key of nextPublicVars) {
const suffix = key.replace('NEXT_PUBLIC_', '')
if (sensitiveSuffixes.some(s => suffix.toUpperCase().includes(s))) {
console.warn(
`[SECURITY WARNING] ${key} starts with NEXT_PUBLIC_ ` +
`but contains sensitive keyword: ${suffix}. ` +
`This will be exposed in the client bundle!`
)
}
}
}
}
validateEnv()
▶ Example: Checking Exposed Variables in a Bundle
# Check the client after building JS Does it contain a key?
npm run build
# Search for sensitive keywords in build artifacts
grep -r "sk_live_" .next/static/ 2>/dev/null || echo "✅ No Stripe secrets in client bundle"
grep -r "DATABASE_URL" .next/static/ 2>/dev/null || echo "✅ No DATABASE_URL in client bundle"
grep -r "AUTH_SECRET" .next/static/ 2>/dev/null || echo "✅ No AUTH_SECRET in client bundle"
Output:
▲ Next.js 16.0.0
Creating an optimized production build ...
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (5/5)
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 5.1 kB 89 kB
├ ○ /about 2.3 kB 86 kB
└ λ /api/items 0 B 84 kB
○ (Static) prerendered as static content
λ (Dynamic) server-rendered on demand
✅ No Stripe secrets in client bundle
✅ No DATABASE_URL in client bundle
✅ No AUTH_SECRET in client bundle
5. Dependency Security Audits
(1) npm audit
# Basic Auditing
npm audit
# Filter by severity level
npm audit --audit-level=high
# Auto-Repair(May break compatibility)
npm audit fix
# Fix only patch Version
npm audit fix --target=patch
# Generate JSON Report
npm audit --json > security-report.json
(2) Snyk integration
# Installation Snyk CLI
npm install -g snyk
# Certification
snyk auth
# Test Items
snyk test
# Continuous Monitoring
snyk monitor
# Docker Mirror Scan
snyk container test taskflow:latest --file=Dockerfile
# .github/workflows/security.yml
name: Security Audit
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Every Monday morning 6 AM
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run npm audit
run: npx audit-ci --high --report-type summary
continue-on-error: true
- name: Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: snyk.sarif
| Tool | Detection Scope | CI Integration | Recommended Fixes |
|---|---|---|---|
| npm audit | Node.js Dependencies | ✅ Built-in | npm audit fix |
| Snyk | Dependencies + Docker + IaC | ✅ Action | Automatic PR Fix |
| GitHub Dependabot | Dependency versions | ✅ Built-in | Automatic PR upgrades |
| CodeQL | Source Code Vulnerability | ✅ Action | Code-Level Fix |
6. v14 to v15 Migration Guide
The biggest change in Next.js 15 is the asynchronous handling of params, cookies, and headers—this is the most disruptive change.
graph TB
subgraph "v14 Synchronize API(Obsolete)"
A[params.id]
B[cookies().get()]
C[headers().get()]
end
subgraph "v15 Asynchronous API(Must)"
D[await params]
E[await cookies()]
F[await headers()]
end
G[generateMetadata It is also necessary to await]
H[Change all dynamic routes to async]
D --> G
E --> G
F --> H
style A fill:#f8d7da
style B fill:#f8d7da
style C fill:#f8d7da
style D fill:#d4edda
style E fill:#d4edda
style F fill:#d4edda
(1) Migration Checklist
| Change | v14 (Old) | v15 (New) | Affected Files |
|---|---|---|---|
| Support | params.id (Synchronous) |
await params (Asynchronous) |
page.tsx, layout.tsx |
| searchParams | searchParams.page (synchronous) |
await searchParams (asynchronous) |
page.tsx |
| cookies | cookies().get()(synchronous) |
await cookies()(asynchronous) |
layout.tsx、middleware.ts |
| headers | headers().get() (synchronous) |
await headers() (asynchronous) |
layout.tsx, page.tsx |
| generateMetadata | params synchronous |
params asynchronous |
page.tsx |
| React | React 18 | React 19 | Global |
| useFormState | react-dom |
Renamed to useActionState |
Form component |
(2) Example of Synchronous to Asynchronous Migration
// ❌ v14 (old) — Synchronous params
export default function ProjectPage({
params
}: {
params: { id: string }
}) {
return <div>Project {params.id}</div>
}
// ✅ v15 (new) — Asynchronous params
export default async function ProjectPage({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return <div>Project {id}</div>
}
// ❌ v14 — Synchronize generateMetadata
export function generateMetadata({ params }: { params: { id: string } }) {
return { title: `Project ${params.id}` }
}
// ✅ v15 — Asynchronous generateMetadata
export async function generateMetadata({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return { title: `Project ${id}` }
}
▶ Example: Asynchronous Handling of Cookies and Headers
Output:
Renders the generateMetadata component UI.
// ❌ v14 — Synchronous Retrieval cookies
import { cookies } from 'next/headers'
export function Layout({ children }: { children: React.ReactNode }) {
const token = cookies().get('session_token')
return <div>{children}</div>
}
// ✅ v15 — Asynchronous Retrieval cookies
import { cookies } from 'next/headers'
export default async function Layout({
children
}: {
children: React.ReactNode
}) {
const cookieStore = await cookies()
const token = cookieStore.get('session_token')
return <div>{children}</div>
}
Output:
Renders the Layout component UI.
7. v15 to v16 Migration Guide
Next.js 16 introduces three major changes: proxy.ts replacing middleware, Turbopack as the default, and the migration of use cache.
graph LR
A[v15] --> B{v16 Migration}
B --> C[proxy.ts Replace middleware<br/>Configurable Routing Rules]
B --> D[Turbopack Default<br/>Remove webpack Layout]
B --> E[use cache Migration<br/>fetch cache → cacheTag]
C --> F[More Declarative Configuration]
D --> G[Build Faster 10x]
E --> H[More Granular Cache Control]
style A fill:#f8d7da
style C fill:#d4edda
style D fill:#d4edda
style E fill:#d4edda
| Change | v15 (Old) | v16 (New) | Migration Action |
|---|---|---|---|
| Route Interception | middleware.ts (Functional) |
proxy.ts (Configurational) |
Extract middleware logic to proxy.ts |
| Build Tools | webpack (default) | Turbopack (default) | Remove custom webpack configuration |
| Caching Model | fetch Automatic Caching |
use cache Instructions |
Move data cache to cacheTag |
| React | React 19 RC | React 19 Stable | Upgrade version |
| PPR | experimental.ppr |
Stable version | Removed the experimental. prefix |
(1) proxy.ts Configuration (v16 New API)
// src/proxy.ts — v16 Replace middleware.ts
// ============================================
// proxy.ts This is a declarative routing rule configuration
// ============================================
import { defineProxy } from 'next/proxy'
export default defineProxy({
// Route Matching Rules
matcher: [
// Paths Requiring Authentication
{ pathname: '/dashboard/:path*', requireAuth: true },
{ pathname: '/projects/:path*', requireAuth: true },
{ pathname: '/settings/:path*', requireAuth: true },
// Redirect Rules
{ pathname: '/old-blog/:slug*', redirect: '/blog/:slug*' },
// Skip static resources
{ pathname: '/_next/:path*', bypass: true },
{ pathname: '/favicon.ico', bypass: true }
],
// Certification Inspection
async auth(request) {
const session = await getSession(request)
if (!session) {
return new Response(null, {
status: 307,
headers: { Location: '/login' }
})
}
return null // Through
},
// Request Header Injection
async headers(request) {
return {
'x-request-id': crypto.randomUUID(),
'x-user-locale': getPreferredLocale(request)
}
}
})
// Utility Functions
async function getSession(request: Request) {
const cookie = request.headers.get('cookie') || ''
const token = cookie.split('session_token=')?.[1]?.split(';')?.[0]
if (!token) return null
return { userId: 'user_123', role: 'admin' }
}
function getPreferredLocale(request: Request) {
const acceptLang = request.headers.get('accept-language') || 'en'
return acceptLang.split(',')[0].split('-')[0]
}
(2) use cache Migration
// ❌ v15 — Usage fetch cache
async function getProjects() {
const res = await fetch('https://api.example.com/projects', {
next: { revalidate: 300 }
})
return res.json()
}
// ✅ v16 — Usage use cache + cacheTag
import { cacheTag, cacheLife } from 'next/cache'
async function getProjects() {
'use cache'
cacheTag('projects')
cacheLife('hours')
const res = await fetch('https://api.example.com/projects')
return res.json()
}
(3) Turbopack Default Configuration
// next.config.js — v16 Simplified Version
/** @type {import('next').NextConfig} */
const nextConfig = {
// v16 Turbopack It is the default.,No configuration required
// Remove the following v15 Layout:
// swcMinify: true, // Turbopack Default
// compiler: { ... }, // Already built-in
// webpack: (config) => config, // Remove Custom webpack
output: 'standalone',
// v16 PPR Stable
ppr: true,
// Retain necessary customizations
serverExternalPackages: ['@prisma/client'],
images: {
formats: ['image/avif', 'image/webp']
}
}
module.exports = nextConfig
▶ Example: Comparison of the system before and after a complete migration
Output:
Next.js configuration applied. Changes take effect on server restart.
// === v14 Original Code ===
// page.tsx
export default function Page({ params }: { params: { id: string } }) {
return <div>{params.id}</div>
}
// layout.tsx
import { cookies } from 'next/headers'
export default function Layout({ children }: { children: React.ReactNode }) {
const theme = cookies().get('theme')
return <div className={theme?.value}>{children}</div>
}
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
if (!request.cookies.has('token')) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
// === v16 After the migration ===
// page.tsx
export default async function Page({
params
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return <div>{id}</div>
}
// layout.tsx
import { cookies } from 'next/headers'
export default async function Layout({
children
}: {
children: React.ReactNode
}) {
const cookieStore = await cookies()
const theme = cookieStore.get('theme')
return <div className={theme?.value}>{children}</div>
}
// proxy.ts(Replace middleware.ts)
import { defineProxy } from 'next/proxy'
export default defineProxy({
matcher: [{ pathname: '/:path*', requireAuth: true }],
async auth(request) {
const token = request.headers.get('cookie')?.includes('token')
if (!token) return Response.redirect('/login')
return null
}
})
Output:
Renders the ▶ Example: Comparison of the system before and after a complete migration component UI as described in the section.
8. Complete Example: Security Hardening + Version Migration Workflow
# ============================================
# security-migration.sh
# Features:Security Hardening + Complete Script for Version Migration
# ============================================
#!/bin/bash
set -euo pipefail
echo "=== TaskFlow Security & Migration ==="
# Phase 1: Security Hardening
echo ""
echo "--- Phase 1: Security Hardening ---"
# 1.1 Runtime Dependency Audit
echo "Running npm audit..."
npm audit --audit-level=high
npm audit fix --target=minor || true
# 1.2 Header Validation
echo "Generating security headers report..."
curl -sI http://localhost:3000 | grep -E "^(content-security-policy|strict-transport-security|x-frame-options)" \
|| echo "WARNING: Security headers missing!"
# 1.3 Inspection NEXT_PUBLIC Leak
echo "Checking for leaked secrets..."
grep -r "NEXT_PUBLIC_.*SECRET\|NEXT_PUBLIC_.*KEY\|NEXT_PUBLIC_.*PASSWORD" \
.env* 2>/dev/null && echo "WARNING: Secrets in NEXT_PUBLIC!" || echo "✅ No leaked secrets"
# 1.4 Verification XSS Protection
echo "Verifying XSS protection..."
# Test Input Encoding
echo "<script>alert('xss')</script>" | node -e "
const { escape } = require('querystring')
process.stdin.on('data', d => console.log('Encoded:', escape(d.toString())))
"
# Phase 2: Version Migration
echo ""
echo "--- Phase 2: Version Migration ---"
# 2.1 Check the current version
CURRENT_VERSION=$(node -e "console.log(require('./node_modules/next/package.json').version)")
echo "Current Next.js version: v$CURRENT_VERSION"
# 2.2 Backup
echo "Backing up current node_modules..."
mv node_modules node_modules_v${CURRENT_VERSION}.bak
mv package-lock.json package-lock_v${CURRENT_VERSION}.json.bak
# 2.3 Upgrade
echo "Upgrading to Next.js 16..."
npm install next@latest react@latest react-dom@latest @types/react@latest @types/react-dom@latest
# 2.4 Run codemod
echo "Running codemods..."
npx @next/codemod@latest built-in-next-font .
npx @next/codemod@latest metadata-to-viewport-export .
npx @next/codemod@latest async-request-params .
# 2.5 Convert middleware to proxy.ts
if [ -f src/middleware.ts ]; then
echo "Converting middleware.ts to proxy.ts..."
mv src/middleware.ts src/middleware_v15.bak.ts
fi
# 2.6 Build and Verify
echo "Building with Next.js 16..."
npm run build
# 2.7 Run a complete test
echo "Running test suite..."
npm test
npm run test:e2e
echo ""
echo "=== Migration Complete ==="
echo "Next.js $(node -e "console.log(require('./node_modules/next/package.json').version)")"
echo "React $(node -e "console.log(require('./node_modules/react/package.json').version)")"
echo "Build: ✅"
echo "Tests: ✅"
echo "Security: ✅"
// src/app/security/report/page.tsx
// ============================================
// Security Report Page
// ============================================
import { headers } from 'next/headers'
import { execSync } from 'child_process'
async function checkSecurityHeaders() {
const h = await headers()
const requiredHeaders = [
'content-security-policy',
'strict-transport-security',
'x-frame-options',
'x-content-type-options',
'referrer-policy',
'permissions-policy'
]
const results = requiredHeaders.map(name => ({
name,
present: h.has(name),
value: h.get(name) || ''
}))
const score = results.filter(r => r.present).length / results.length * 100
return { results, score }
}
async function checkDependencies() {
try {
const auditOutput = execSync('npm audit --json --audit-level=high', {
encoding: 'utf-8',
timeout: 30000
}).toString()
const audit = JSON.parse(auditOutput)
return {
vulnerabilities: audit.metadata?.vulnerabilities || { high: 0, critical: 0 },
total: audit.metadata?.totalDependencies || 0
}
} catch {
return { vulnerabilities: { high: 0, critical: 0 }, total: 0 }
}
}
const gradeMap: Record<number, string> = {
100: 'A+', 90: 'A', 80: 'B', 70: 'C', 60: 'D', 0: 'F'
}
function getGrade(score: number): string {
for (const [threshold, grade] of Object.entries(gradeMap)) {
if (score >= Number(threshold)) return grade
}
return 'F'
}
export default async function SecurityReportPage() {
const [securityCheck, deps] = await Promise.all([
checkSecurityHeaders(),
checkDependencies()
])
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold">Security Report</h1>
{/* HTTP Safety Rating */}
<div className="bg-white rounded-lg shadow p-4">
<h2 className="text-lg font-semibold mb-4">
HTTP Security Headers
<span className="ml-2 px-2 py-1 text-sm rounded bg-blue-100">
Grade: {getGrade(securityCheck.score)}
</span>
</h2>
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-500">
<th className="pb-2">Header</th>
<th className="pb-2">Status</th>
<th className="pb-2">Value</th>
</tr>
</thead>
<tbody>
{securityCheck.results.map(r => (
<tr key={r.name} className="border-t">
<td className="py-2 font-mono">{r.name}</td>
<td className="py-2">
{r.present ? '✅' : '❌'}
</td>
<td className="py-2 text-xs text-gray-500 truncate max-w-md">
{r.value || '—'}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Dependency Security */}
<div className="bg-white rounded-lg shadow p-4">
<h2 className="text-lg font-semibold mb-4">Dependency Audit</h2>
<p>Total packages: {deps.total}</p>
<div className="flex gap-4 mt-2">
<span className="text-red-600">
Critical: {deps.vulnerabilities.critical}
</span>
<span className="text-orange-600">
High: {deps.vulnerabilities.high}
</span>
</div>
</div>
{/* Environment Variable Check */}
<div className="bg-white rounded-lg shadow p-4">
<h2 className="text-lg font-semibold mb-4">Environment Variables</h2>
<p className="text-green-600">✅ No NEXT_PUBLIC_ secrets detected</p>
</div>
</div>
)
}
❓ FAQ
next.config.js, use the Content-Security-Policy-Report-Only header in conjunction with report-uri or report-to to send reports to Sentry or a custom endpoint.params?generateMetadata, layout.tsx, and page.tsx. If your project contains a large number of pages that use params.id, you’ll need to check them one by one during the migration. We recommend running the npx @next/codemod@latest async-request-params automated migration first, then manually checking any uncovered edge cases.proxy.ts and the original middleware.ts functionally equivalent?proxy.ts uses declarative configuration (focusing on "what rules"), while middleware.ts uses imperative functions (focusing on "how to do it"). proxy.ts covers 90% of middleware scenarios (authentication, redirection, header injection), but for complex request rewriting logic (such as i18n route rewriting), you’ll still need to use middleware.ts (which is still supported in v16).next.config.js using experimental.turbo.rules. The Vercel team plans to implement full compatibility with Webpack configurations in v16.3.use cache cache and the previous fetch cache?fetch cache is automatic and implicit (default force-cache), and cannot be finely controlled. use cache is an explicit, component-level caching directive that supports cacheTag (expiration by label) and cacheLife (lifecycle policies). Migration path: (1) Simple fetch caching can be retained; (2) Scenarios requiring on-demand invalidation should migrate to use cache + cacheTag; (3) New code should use use cache directly.git log -p -S 'sk_live_'); (3) If the secret was ever exposed under NEXT_PUBLIC_, assume that all past visitors may have accessed it and rotate it; (4) Add github-secret-scanning and .env* to .gitignore to prevent another leak.📖 Summary
- HTTP security headers (CSP, HSTS, X-Frame-Options, etc.) are configured in the
next.config.jsfunction ofheadersand serve as the first line of defense against XSS and clickjacking. - Variables prefixed with
NEXT_PUBLIC_are exposed to client-side JavaScript; never use them to store keys..env*.localMust be included.gitignore npm auditand Snyk perform regular scans for dependency vulnerabilities and integrate security audit checks into CI- v14→v15 Migration Highlights:
params/cookies/headershave all been converted to asynchronous;useFormStatehas been renamed touseActionState - Key changes in the v15→v16 migration:
proxy.tsdeclarative configuration replacesmiddleware.tsfunctional middleware; Turbopack becomes the default build tool;use cachedirectives replace implicitfetchcaching - Security hardening and version migration should be carried out in phases: first, fix high-risk vulnerabilities; then, upgrade the version; each step should undergo thorough testing and validation.
📝 Exercises
-
Basic Exercise (⭐): Configure the six security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) in
next.config.js, then usecurl -Ior Mozilla Observatory to verify that the configuration is active. -
Advanced Exercise (⭐⭐): Migrate a project using Next.js 14 to v16: (1) Run
npx @next/codemod async-request-paramsto perform an automatic migration; (2) Manually fix any uncoveredcookies()andheaders()synchronous calls; (3) Convertmiddleware.tstoproxy.ts(authentication route protection); (4) Remove thewebpackconfiguration and verify that the Turbopack build succeeds. -
Challenge (⭐⭐⭐): Implement a complete secure CI/CD pipeline: (1) Add
npm auditaccess control to GitHub Actions (block merges with a severity level of “high” or higher); (2) Integrate Snyk scans and automatically comment on vulnerability reports in pull requests; (3) Execute a security header validation script after the build and search the build artifacts forNEXT_PUBLIC_leaks; (4) Automatically run a full security audit weekly and generate an HTML report.



