Next.js Security Checklist for AI-Generated Projects

Written by the Rafter Team

Next.js is the default framework for most vibe coding platforms, and AI-generated Next.js code ships with predictable security gaps. Two critical vulnerabilities disclosed in 2025 underscore the stakes: CVE-2025-29927 allowed complete middleware bypass with a single HTTP header (CVSS 9.1), and CVE-2025-55182 enabled unauthenticated remote code execution through React Server Components (CVSS 10.0). AI assistants don't track CVEs, don't update dependencies proactively, and don't implement security headers unless you ask. This checklist covers every security control your vibe-coded Next.js app needs.
If your project uses Next.js versions 11.1.4 through 15.2.2, your middleware-based authentication is bypassable. Update immediately or block the x-middleware-subrequest header at your reverse proxy.
Introduction
AI coding assistants generate functional Next.js applications. They scaffold pages, create API routes, wire up Server Actions, and connect to databases. What they consistently skip: authentication middleware, security headers, input validation, CSRF protection, and proper environment variable handling.
This checklist is organized by risk priority. Work through it top to bottom—the first sections address the vulnerabilities most likely to be exploited.
Critical: Framework Version and Known CVEs
Update Next.js
AI assistants scaffold projects with whatever version was current when their training data was collected. Check your version and update:
# Check current version
npx next --version
# Update to latest
npm install next@latest react@latest react-dom@latest
Known Critical Vulnerabilities
| CVE | Severity | Affected Versions | Impact |
|---|---|---|---|
| CVE-2025-29927 | 9.1 Critical | 11.1.4–15.2.2 | Middleware bypass via x-middleware-subrequest header |
| CVE-2025-55182 | 10.0 Critical | React RSC + Next.js | Unauthenticated RCE via crafted HTTP request |
| CVE-2025-66478 | 10.0 Critical | Next.js with RSC | RCE in Next.js's RSC implementation |
- Running Next.js 15.2.3 or later
- Running React 19.0.1 or later (if using RSC)
- No known critical CVEs in
npm auditoutput
Critical: Authentication and Authorization
AI assistants create routes and Server Actions without auth checks. Every endpoint is a public API unless you explicitly protect it.
Protect All Server Actions
// ✗ Vulnerable: AI-generated Server Action
'use server'
export async function updateProfile(data: FormData) {
const name = data.get('name') as string;
await db.update(users).set({ name }).where(eq(users.id, data.get('id')));
}
// ✓ Secure: Auth check + ownership verification
'use server'
export async function updateProfile(data: FormData) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
const targetId = data.get('id') as string;
if (session.user.id !== targetId) throw new Error('Forbidden');
const name = data.get('name') as string;
await db.update(users).set({ name }).where(eq(users.id, targetId));
}
Protect All API Routes
// ✗ Vulnerable: No auth check
export async function GET(request: Request) {
const users = await db.select().from(usersTable);
return Response.json(users);
}
// ✓ Secure: Auth required
export async function GET(request: Request) {
const session = await auth();
if (!session?.user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const userData = await db.select()
.from(usersTable)
.where(eq(usersTable.id, session.user.id));
return Response.json(userData);
}
Auth Checklist
- Every Server Action checks
auth()before mutating data - Every API route handler checks authentication
- Admin routes verify admin role, not just authentication
- Ownership verified before data mutations (user can only update their own data)
- Middleware protects route groups, not individual pages
Critical: Input Validation
AI assistants pass user input directly to database queries and business logic. Always validate at the boundary.
Validate Server Action Inputs
import { z } from 'zod';
const updateProfileSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
bio: z.string().max(500).optional(),
});
'use server'
export async function updateProfile(data: FormData) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
const parsed = updateProfileSchema.safeParse({
name: data.get('name'),
email: data.get('email'),
bio: data.get('bio'),
});
if (!parsed.success) {
return { error: parsed.error.flatten() };
}
await db.update(users)
.set(parsed.data)
.where(eq(users.id, session.user.id));
}
Validate API Route Parameters
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
// ✗ Never trust route params
// const user = await db.query(`SELECT * FROM users WHERE id = '${params.id}'`);
// ✓ Validate and use parameterized queries
const id = z.string().uuid().safeParse(params.id);
if (!id.success) {
return Response.json({ error: 'Invalid ID' }, { status: 400 });
}
const user = await db.select()
.from(usersTable)
.where(eq(usersTable.id, id.data));
return Response.json(user);
}
Input Validation Checklist
- All Server Action inputs validated with Zod or similar
- All API route parameters validated
- Database queries use parameterized statements (no string interpolation)
- File uploads restricted by type, size, and count
- Search inputs sanitized to prevent injection
High: Security Headers
AI-generated Next.js apps ship with zero security headers. Add them in next.config.js:
// 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: 'origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
];
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
Content Security Policy
For apps with inline scripts or third-party resources, add a CSP header. Use a nonce-based approach in middleware:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
font-src 'self';
connect-src 'self' https://*.supabase.co;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`.replace(/\n/g, '');
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', cspHeader);
response.headers.set('x-nonce', nonce);
return response;
}
Security Headers Checklist
-
Strict-Transport-Securityheader set -
X-Frame-Optionsset to SAMEORIGIN -
X-Content-Type-Optionsset to nosniff -
Referrer-Policyconfigured -
Permissions-Policydisables unused browser features - Content Security Policy configured for your resource origins
High: Environment Variables
AI assistants frequently mishandle Next.js environment variables, either exposing server secrets to the client or failing to use them at all.
The NEXT_PUBLIC_ Prefix Rule
In Next.js, only environment variables prefixed with NEXT_PUBLIC_ are available in the browser. Variables without the prefix are server-only.
# Server-only (never sent to browser)
DATABASE_URL=postgresql://...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
STRIPE_SECRET_KEY=sk_live_...
# Client-accessible (bundled into JavaScript)
NEXT_PUBLIC_SUPABASE_URL=https://abc.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
Common AI Mistakes
// ✗ Dangerous: Server secret without NEXT_PUBLIC_ used in client component
'use client'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // undefined in browser, but risky pattern
// ✗ Dangerous: Service role key exposed with NEXT_PUBLIC_
const supabase = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY);
// ✓ Correct: Server secrets in server-only code
// app/api/payment/route.ts (API route - server only)
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
Environment Variables Checklist
- No server secrets use the
NEXT_PUBLIC_prefix -
.env,.env.local,.env.productionall in.gitignore -
service_rolekey never exposed withNEXT_PUBLIC_ - No hardcoded keys in source files (search for
sk_,eyJ,ghp_,AKIA) - Production environment variables set in hosting provider, not in files
Medium: CSRF and Rate Limiting
CSRF Protection
Next.js Server Actions include built-in CSRF protection via the Origin header check. But custom API routes don't:
// API route with manual CSRF check
export async function POST(request: Request) {
const origin = request.headers.get('origin');
if (origin !== process.env.NEXT_PUBLIC_APP_URL) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
// ... handle request
}
Rate Limiting
AI-generated APIs ship without rate limiting. Use middleware or a library like next-rate-limit:
// Simple in-memory rate limiter for API routes
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
function rateLimit(ip: string, limit = 10, windowMs = 60000): boolean {
const now = Date.now();
const entry = rateLimitMap.get(ip);
if (!entry || now - entry.timestamp > windowMs) {
rateLimitMap.set(ip, { count: 1, timestamp: now });
return true;
}
if (entry.count >= limit) return false;
entry.count++;
return true;
}
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
if (!rateLimit(ip, 5, 60000)) {
return Response.json({ error: 'Too many requests' }, { status: 429 });
}
// ... handle request
}
CSRF and Rate Limiting Checklist
- Custom API routes verify
Originheader - Authentication endpoints rate-limited (5 attempts per minute)
- Expensive operations rate-limited (file uploads, AI calls, emails)
- Rate limit responses include
Retry-Afterheader
Medium: Error Handling and Information Disclosure
AI assistants often return raw error messages that leak implementation details:
// ✗ Vulnerable: Leaks database schema and connection details
export async function GET() {
try {
const data = await db.select().from(users);
return Response.json(data);
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
}
// ✓ Secure: Generic error to client, detailed log to server
export async function GET() {
try {
const data = await db.select().from(users);
return Response.json(data);
} catch (error) {
console.error('Database query failed:', error);
return Response.json(
{ error: 'An internal error occurred' },
{ status: 500 }
);
}
}
Error Handling Checklist
- No raw error messages returned to clients in production
- Stack traces disabled in production (
NODE_ENV=production) - Database errors logged server-side only
- Custom error pages configured (
not-found.tsx,error.tsx) - Sensitive paths return 404 instead of 403 (don't reveal existence)
Low: Dependency Security
Audit and Update
# Check for known vulnerabilities
npm audit
# Fix automatically where possible
npm audit fix
# Check for outdated packages
npm outdated
Dependency Checklist
-
npm auditshows no critical or high vulnerabilities -
package-lock.jsoncommitted to version control - No unused dependencies in
package.json - Dependabot or Renovate configured for automated updates
The Complete Checklist
Copy this into your project and check off each item before shipping:
## Pre-Ship Security Checklist
### Critical
- [ ] Next.js 15.2.3+ (middleware bypass fixed)
- [ ] React 19.0.1+ (RSC RCE fixed)
- [ ] Every Server Action checks auth()
- [ ] Every API route checks authentication
- [ ] Admin routes verify admin role
- [ ] All inputs validated with Zod
- [ ] Database queries parameterized
### High
- [ ] Security headers configured
- [ ] CSP header set
- [ ] No server secrets with NEXT_PUBLIC_ prefix
- [ ] .env files in .gitignore
- [ ] No hardcoded keys in source
### Medium
- [ ] CSRF protection on custom API routes
- [ ] Rate limiting on auth endpoints
- [ ] Generic error messages in production
- [ ] Custom error pages configured
### Low
- [ ] npm audit clean
- [ ] Lock file committed
- [ ] Unused dependencies removed
Conclusion
AI coding assistants produce functional Next.js applications. They don't produce secure ones. The checklist above covers the specific gaps that AI assistants consistently leave open—from the critical framework vulnerabilities to the mundane-but-exploitable missing security headers.
Your next steps:
- Update Next.js and React—the CVEs from 2025 are actively exploited and trivially reproducible
- Audit every Server Action and API route for auth checks—this is the most common vulnerability in AI-generated Next.js code
- Add security headers to
next.config.js—copy the configuration above, it takes two minutes - Run Rafter against your repo—automated scanning catches the vulnerabilities this checklist describes, plus dependency and secrets issues
Scan your Next.js project with Rafter →
Related Resources
- Vibe Coding Security: The Complete Guide
- Supabase RLS for Vibe-Coded Apps: The Security You're Missing
- Secure Prompting Patterns for AI Code Generation
- How to Thoroughly Test Your Vibe-Coded App
- Vibe Coding Is Great — Until It Isn't: Why Security Matters
- Security Audit: v0
- Automated Security Scanning for Modern Applications