Next Security Hardening¶
Server Components are secure by default. Client Components leak everything you pass to them. The render tree doesn't care about your secrets - it renders whatever you give it and ships it to the browser
Next.js improves security over plain React because more code runs server-side. But framework-level security doesn't fix application-level mistakes. You can still leak data , miss CSRF , forget CSP headers , and expose admin routes. The framework gives you tools - you have to use them
Server Components - the security boundary¶
Server Components never ship to the client. Their imports , data fetching , and logic stay on the server
// This is SECURE - all server-side
// src/app/page.tsx (Server Component)
import { getSecretData } from '@/lib/db'
export default async function Page() {
const data = await getSecretData() // never reaches client
return <div>{data.publicField}</div> // only public field rendered
}
The danger: passing server data to a Client Component
// This is DANGEROUS - server data passed to client
// src/app/page.tsx
import { getSecretData } from '@/lib/db'
import ClientWidget from './ClientWidget'
export default async function Page() {
const data = await getSecretData()
// data contains password_hash , ssn , internal notes
return <ClientWidget data={data} /> // ALL of data ships to client!
}
// ClientWidget.tsx
'use client'
export default function ClientWidget({ data }: { data: any }) {
// Data includes password_hash!
// Available in browser devtools , React DevTools , network tab
return <div>{data.name}</div>
}
The fix: never pass full database objects to Client Components. Extract only what the client needs
// Secure version - pass only what's needed
export default async function Page() {
const data = await getSecretData()
return <ClientWidget name={data.name} avatar={data.avatar} />
}
CSRF protection¶
Server Actions: Built-in CSRF protection. Next.js generates a crypto-signed token and validates it on every action call
Route handlers: NO built-in CSRF protection. If you use cookie auth with route handlers , you need SameSite cookies or custom CSRF tokens
// Route handler with SameSite protection
// src/app/api/transfer/route.ts
export async function POST(request: Request) {
// Check Origin/Referer headers manually
const origin = request.headers.get('origin')
const referer = request.headers.get('referer')
const allowedOrigin = process.env.APP_URL
if (origin && !origin.startsWith(allowedOrigin)) {
return Response.json({ error: 'Cross-origin request blocked' }, { status: 403 })
}
if (referer && !referer.startsWith(allowedOrigin)) {
return Response.json({ error: 'Invalid referer' }, { status: 403 })
}
// Process the request
const body = await request.json()
// ... transfer logic ...
}
SameSite cookie configuration:
// In your auth setup or route handler
response.cookies.set('session', token, {
httpOnly: true, // not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'lax', // or 'strict' for maximum protection
path: '/'
})
SameSite=Strict prevents cookies from being sent on any cross-site request. SameSite=Lax allows cookies on top-level navigations (standard OAuth flows need this). SameSite=None disables SameSite - only use this with Secure flag and explicit CSRF tokens
XSS in Server vs Client Components¶
Server Components render HTML strings that are escaped by default. No XSS via data interpolation
// Server Component - safe by default
export default async function Page() {
const userInput = '<script>alert("xss")</script>'
return <div>{userInput}</div>
// Renders: <div><script>alert("xss")</script></div>
// Safe. Escaped.
}
Client Components with dangerouslySetInnerHTML are NOT safe:
'use client'
export default function RichContent({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />
// If html contains <script> tags , they execute
// Sanitize with DOMPurify before rendering
}
// Safe version
import DOMPurify from 'isomorphic-dompurify'
export default function RichContent({ html }: { html: string }) {
const sanitized = DOMPurify.sanitize(html)
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />
}
data leaks prevention¶
Checklist for preventing data leaks:
- Never pass full DB objects to Client Components - extract specific fields
- NEXT_PUBLIC_ prefix audit - anything with this prefix ships to the browser. API keys , secrets , database URLs should never have this prefix
- Bundle analysis - run
next buildand inspect the bundle. If you see server-only code in the client bundle , you imported it wrong - Console.log in Server Components - server logs are fine. Client Component server logs are NOT - they print in the browser console
- Error messages - Server Component errors show in development. In production , customize the error page to avoid leaking stack traces
// src/app/error.tsx
'use client'
export default function ErrorPage({ error }: { error: Error }) {
// Don't show error.message to users - it might contain SQL , paths , or secrets
return (
<div>
<h2>Something went wrong</h2>
<p>Our team has been notified. Try again later</p>
</div>
)
}
CSP headers¶
Content Security Policy headers prevent XSS by telling the browser what sources are trusted
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set(
'Content-Security-Policy',
[
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'", // unsafe-inline needed for Next.js
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-src 'none'",
"object-src 'none'"
].join('; ')
)
return response
}
Or set CSP in next.config.ts:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
}
]
}
]
}
}
export default nextConfig
CSP is one of those things every security guide mentions and most production apps don't have. Be the exception. Add it
prerequisites¶
next_09_auth.md - you should understand authentication flows and session handling
next → next_11_middleware.md