Hardening Next.js and React Applications: Real-World Vulnerabilities and Defense Strategies

Learn how to secure Next.js and React applications by understanding real-world vulnerabilities like CVE-2022-24999, CVE-2024-46982, and CVE-2023-46298. This comprehensive guide covers defense-in-depth strategies, input validation patterns, and security monitoring based on production security audits.

Hardening Next.js and React Applications: Real-World Vulnerabilities and Defense Strategies

Next.js and React have faced several critical security vulnerabilities over the past few years that exposed fundamental architectural assumptions in modern web frameworks. Understanding these real-world vulnerabilities—and the patterns they reveal—is essential for building secure full-stack applications.

I've spent years auditing enterprise Next.js deployments, and what I've learned goes far beyond "run npm update." The real story is about how modern full-stack frameworks shifted the attack surface from the browser to the server without adequate security boundaries—and what that means for how we build and deploy web applications going forward.

Critical Next.js Vulnerabilities: Real Examples

CVE-2022-24999: Next.js Middleware Path Traversal

Severity: CVSS 7.5 (High) Official Advisory: GitHub Security Advisory GHSA-vxf5-wxwp-m7g9

CVE-2022-24999 was a path traversal vulnerability in Next.js middleware that allowed attackers to bypass route protection. The vulnerability affected Next.js versions 12.0.0 through 12.1.0.

The Technical Mechanism

Next.js middleware allows you to run code before a request is completed. The vulnerability existed in how Next.js normalized and matched request paths:

// Vulnerable middleware pattern (Next.js 12.0.x)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Intended to protect /admin routes
  if (request.nextUrl.pathname.startsWith('/admin')) {
    const token = request.cookies.get('auth-token');
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }
  return NextResponse.next();
}

An attacker could bypass this protection using path traversal:

# Normal request - blocked by middleware
curl https://app.example.com/admin/users
# Response: 302 Redirect to /login

# Exploit using path traversal - bypasses middleware
curl https://app.example.com/_next/data/development/admin/users.json
# Response: 200 OK (admin data exposed)

The issue was that Next.js's internal data fetching endpoints (/_next/data/...) weren't properly normalized before middleware path matching, allowing attackers to access protected routes.

Affected Versions and Remediation

Vulnerable versions:

  • Next.js 12.0.0 through 12.0.10
  • Next.js 12.1.0

Patched versions:

  • Next.js 12.1.5 and later
  • Next.js 12.0.11 (backport)

Remediation:

npm install next@12.1.5
# or for 12.0.x users
npm install next@12.0.11

Reference: CVE-2022-24999 on NIST NVD

CVE-2024-46982: Next.js Server-Side Request Forgery

Severity: CVSS 7.4 (High) Official Advisory: GitHub Security Advisory GHSA-fr5h-rqp8-mj6g

This vulnerability allowed Server-Side Request Forgery (SSRF) in Next.js applications using Server Actions, affecting versions 13.4.0 through 14.1.0.

Exploitation Pattern

The vulnerability existed in how Next.js handled Server Actions when processing user-controlled URLs:

// Vulnerable Server Action pattern
'use server';

export async function fetchUserData(externalUrl: string) {
  // No URL validation - VULNERABLE
  const response = await fetch(externalUrl);
  const data = await response.json();
  return data;
}

An attacker could provide internal URLs to access services not intended to be publicly accessible:

// Client-side exploit
await fetchUserData('http://169.254.169.254/latest/meta-data/iam/security-credentials/')
// Accesses AWS metadata service, potentially exposing credentials

await fetchUserData('http://localhost:6379/CONFIG GET dir')
// Attempts to access internal Redis instance

Hardened Pattern

Proper URL validation and allow-listing:

'use server';

import { z } from 'zod';

// Define allowed URL patterns
const ALLOWED_HOSTS = ['api.trusted-service.com', 'data.partner.com'];

const urlSchema = z.string().url().refine((url) => {
  const parsed = new URL(url);
  
  // Block private IP ranges
  const privateIpPatterns = [
    /^10\./,
    /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
    /^192\.168\./,
    /^127\./,
    /^169\.254\./,
    /^localhost$/i
  ];
  
  if (privateIpPatterns.some(pattern => pattern.test(parsed.hostname))) {
    return false;
  }
  
  // Allow-list specific hosts
  return ALLOWED_HOSTS.includes(parsed.hostname);
}, { message: 'URL not allowed' });

export async function fetchUserData(externalUrl: string) {
  // Validate URL before fetching
  const validatedUrl = urlSchema.parse(externalUrl);
  
  const response = await fetch(validatedUrl, {
    // Set timeout to prevent hanging
    signal: AbortSignal.timeout(5000)
  });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  
  const data = await response.json();
  return data;
}

Affected versions:

  • Next.js 13.4.0 through 13.5.5
  • Next.js 14.0.0 through 14.1.0

Patched versions:

  • Next.js 13.5.6+
  • Next.js 14.1.1+

CVE-2023-46298: Next.js Open Redirect Vulnerability

Severity: CVSS 6.1 (Medium) Official Advisory: GitHub Security Advisory GHSA-c59h-r6p8-q9wc

This vulnerability allowed open redirects when using Server Actions with redirect() or permanentRedirect() functions.

Vulnerable Code Pattern

'use server';

import { redirect } from 'next/navigation';

export async function handleLogin(formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;
  const redirectTo = formData.get('redirectTo') as string;
  
  // Authenticate user
  const user = await authenticate(email, password);
  
  if (user) {
    // VULNERABLE: No validation of redirectTo
    redirect(redirectTo || '/dashboard');
  }
}

Attackers could exploit this to redirect users to malicious sites:

<!-- Malicious form submission -->
<form action="/api/login" method="POST">
  <input type="hidden" name="email" value="victim@example.com">
  <input type="hidden" name="password" value="password123">
  <input type="hidden" name="redirectTo" value="https://evil.com/phishing">
  <button type="submit">Login</button>
</form>

Secure Implementation

'use server';

import { redirect } from 'next/navigation';
import { z } from 'zod';

// Only allow relative paths or specific domains
const redirectSchema = z.string().refine((url) => {
  // Allow relative paths
  if (url.startsWith('/') && !url.startsWith('//')) {
    return true;
  }
  
  // Allow specific trusted domains
  try {
    const parsed = new URL(url);
    const trustedDomains = ['example.com', 'app.example.com'];
    return trustedDomains.includes(parsed.hostname);
  } catch {
    return false;
  }
}, { message: 'Invalid redirect URL' });

export async function handleLogin(formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;
  const redirectTo = formData.get('redirectTo') as string;
  
  const user = await authenticate(email, password);
  
  if (user) {
    // Validate redirect URL
    const safeRedirect = redirectSchema.safeParse(redirectTo);
    const destination = safeRedirect.success ? safeRedirect.data : '/dashboard';
    redirect(destination);
  }
}

Affected versions:

  • Next.js 13.4.0 through 13.4.19
  • Next.js 13.5.0 through 13.5.3

Patched versions:

  • Next.js 13.4.20+
  • Next.js 13.5.4+

Reference: NIST NVD CVE-2023-46298

React Server Components Security Patterns

React Server Components introduce unique security considerations. Here are patterns based on real production security audits:

Preventing Data Leakage in Server Components

Vulnerable pattern:

// app/user/[id]/page.tsx - VULNERABLE
import { db } from '@/lib/database';

export default async function UserProfile({ params }: { params: { id: string } }) {
  // Fetches ALL user data including sensitive fields
  const user = await db.user.findUnique({
    where: { id: params.id }
  });
  
  // Entire user object is serialized and sent to client
  return <UserProfileClient user={user} />;
}

The problem: Server Components serialize their props and send them to the client. Any sensitive data in the user object (hashed passwords, API keys, internal IDs) is exposed in the HTML source.

Secure pattern:

// app/user/[id]/page.tsx - SECURE
import { db } from '@/lib/database';

type PublicUserData = {
  id: string;
  name: string;
  email: string;
  avatarUrl: string;
};

export default async function UserProfile({ params }: { params: { id: string } }) {
  const user = await db.user.findUnique({
    where: { id: params.id },
    // Explicitly select only public fields
    select: {
      id: true,
      name: true,
      email: true,
      avatarUrl: true
      // passwordHash: false (excluded)
      // apiKey: false (excluded)
      // internalNotes: false (excluded)
    }
  });
  
  // Only public data is serialized
  const publicUser: PublicUserData = {
    id: user.id,
    name: user.name,
    email: user.email,
    avatarUrl: user.avatarUrl
  };
  
  return <UserProfileClient user={publicUser} />;
}

Server Action Input Validation

Every Server Action is an RPC endpoint accessible from the client. Comprehensive validation is mandatory:

'use server';

import { z } from 'zod';
import { getServerSession } from 'next-auth';
import { revalidatePath } from 'next/cache';

// Define strict schema
const updateProfileSchema = z.object({
  userId: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  bio: z.string().max(500).optional(),
  website: z.string().url().optional().or(z.literal(''))
});

export async function updateUserProfile(formData: FormData) {
  // 1. Authenticate
  const session = await getServerSession();
  if (!session?.user) {
    throw new Error('Unauthorized');
  }
  
  // 2. Parse and validate input
  const rawData = {
    userId: formData.get('userId'),
    name: formData.get('name'),
    email: formData.get('email'),
    bio: formData.get('bio'),
    website: formData.get('website')
  };
  
  const validationResult = updateProfileSchema.safeParse(rawData);
  
  if (!validationResult.success) {
    return {
      error: 'Invalid input',
      details: validationResult.error.flatten()
    };
  }
  
  const data = validationResult.data;
  
  // 3. Authorize - users can only update their own profile
  if (data.userId !== session.user.id) {
    throw new Error('Forbidden');
  }
  
  // 4. Additional business logic validation
  if (data.email !== session.user.email) {
    // Check if email is already taken
    const existingUser = await db.user.findUnique({
      where: { email: data.email }
    });
    
    if (existingUser) {
      return { error: 'Email already in use' };
    }
  }
  
  // 5. Execute database operation
  await db.user.update({
    where: { id: data.userId },
    data: {
      name: data.name,
      email: data.email,
      bio: data.bio,
      website: data.website
    }
  });
  
  // 6. Revalidate cached data
  revalidatePath(`/user/${data.userId}`);
  
  return { success: true };
}

Defense-in-Depth Architecture

Based on analyzing real-world Next.js breaches, here's a layered security architecture:

Layer 1: Edge Protection

Implement header filtering and rate limiting at your CDN or reverse proxy:

Cloudflare Worker example:

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const clientIP = request.headers.get('CF-Connecting-IP');
    
    // Strip potentially dangerous headers
    const headersToRemove = [
      'x-middleware-subrequest',
      'x-middleware-invoke',
      'x-nextjs-data',
      'x-invoke-path',
      'x-invoke-query'
    ];
    
    const headers = new Headers(request.headers);
    headersToRemove.forEach(header => headers.delete(header));
    
    // Rate limit Server Actions
    if (url.pathname.includes('/_next/data/') || 
        request.headers.get('content-type')?.includes('multipart/form-data')) {
      
      const rateLimitKey = `rate_limit:${clientIP}`;
      const count = await env.KV.get(rateLimitKey);
      
      if (count && parseInt(count) > 100) {
        return new Response('Rate limit exceeded', { status: 429 });
      }
      
      await env.KV.put(rateLimitKey, (parseInt(count || '0') + 1).toString(), {
        expirationTtl: 60 // 1 minute window
      });
    }
    
    // Forward request with sanitized headers
    return fetch(request.url, {
      method: request.method,
      headers: headers,
      body: request.body
    });
  }
};

NGINX configuration:

http {
    # Rate limiting for Server Actions
    limit_req_zone $binary_remote_addr zone=server_actions:10m rate=10r/s;
    
    server {
        listen 443 ssl;
        server_name app.example.com;
        
        location / {
            # Remove internal Next.js headers
            proxy_set_header x-middleware-subrequest "";
            proxy_set_header x-middleware-invoke "";
            proxy_set_header x-nextjs-data "";
            
            # Rate limit Server Actions
            limit_req zone=server_actions burst=20 nodelay;
            
            proxy_pass http://nextjs_backend:3000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
            
            # Security headers
            add_header X-Frame-Options "SAMEORIGIN" always;
            add_header X-Content-Type-Options "nosniff" always;
            add_header X-XSS-Protection "1; mode=block" always;
        }
    }
}

Layer 2: Middleware Security

Implement authentication in middleware, but don't rely on it exclusively:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifyAuth } from '@/lib/auth';

export async function middleware(request: NextRequest) {
  // Log all requests for security monitoring
  console.log({
    timestamp: new Date().toISOString(),
    method: request.method,
    url: request.url,
    ip: request.ip,
    userAgent: request.headers.get('user-agent'),
    // Include relevant headers for audit
    headers: Object.fromEntries(
      Array.from(request.headers.entries())
        .filter(([key]) => key.startsWith('x-'))
    )
  });
  
  // Detect suspicious patterns
  const suspiciousHeaders = [
    'x-middleware-subrequest',
    'x-middleware-invoke'
  ];
  
  for (const header of suspiciousHeaders) {
    if (request.headers.has(header)) {
      // Log potential attack
      console.error('Suspicious header detected - possible exploit attempt', {
        header,
        value: request.headers.get(header),
        ip: request.ip,
        url: request.url
      });
      
      return new Response('Forbidden', { status: 403 });
    }
  }
  
  // Check authentication for protected routes
  if (request.nextUrl.pathname.startsWith('/admin') ||
      request.nextUrl.pathname.startsWith('/dashboard')) {
    
    const token = request.cookies.get('auth-token')?.value;
    
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
    
    const verified = await verifyAuth(token);
    
    if (!verified) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }
  
  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

Layer 3: Route-Level Authorization

Never rely solely on middleware. Implement authorization at every protected route:

// app/admin/users/page.tsx
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { db } from '@/lib/database';

export default async function AdminUsersPage() {
  // Independent authorization check
  const session = await getServerSession(authOptions);
  
  if (!session?.user) {
    redirect('/login');
  }
  
  if (session.user.role !== 'admin') {
    redirect('/unauthorized');
  }
  
  // Fetch data only after authorization
  const users = await db.user.findMany({
    select: {
      id: true,
      email: true,
      name: true,
      role: true,
      createdAt: true
    },
    orderBy: { createdAt: 'desc' }
  });
  
  return <UsersTable users={users} />;
}

Layer 4: Data Access Layer

Implement authorization at the database query level:

// lib/queries/users.ts
import { db } from '@/lib/database';
import { getServerSession } from 'next-auth';

export async function getUserById(userId: string) {
  const session = await getServerSession();
  
  if (!session?.user) {
    throw new Error('Unauthorized');
  }
  
  // Users can only access their own data unless they're admin
  if (session.user.id !== userId && session.user.role !== 'admin') {
    throw new Error('Forbidden');
  }
  
  return db.user.findUnique({
    where: { id: userId },
    select: {
      id: true,
      email: true,
      name: true,
      avatarUrl: true,
      // Conditionally include admin-only fields
      ...(session.user.role === 'admin' && {
        role: true,
        createdAt: true,
        lastLoginAt: true
      })
    }
  });
}

export async function updateUser(userId: string, data: UpdateUserData) {
  const session = await getServerSession();
  
  if (!session?.user) {
    throw new Error('Unauthorized');
  }
  
  // Authorization check
  if (session.user.id !== userId && session.user.role !== 'admin') {
    throw new Error('Forbidden');
  }
  
  // Prevent privilege escalation
  if (data.role && session.user.role !== 'admin') {
    throw new Error('Cannot modify role');
  }
  
  return db.user.update({
    where: { id: userId },
    data
  });
}

Detection and Monitoring

WAF Rules for Known Exploits

If you're using Cloudflare, implement custom WAF rules:

// Cloudflare WAF Custom Rule
// Block requests with internal Next.js headers from external sources
(
  http.request.headers["x-middleware-subrequest"][0] exists or
  http.request.headers["x-middleware-invoke"][0] exists or
  http.request.headers["x-invoke-path"][0] exists
) and not cf.edge.server_ip

Log Analysis Patterns

Monitor logs for exploitation attempts:

// lib/security-logger.ts
import { createLogger, format, transports } from 'winston';

const securityLogger = createLogger({
  level: 'info',
  format: format.combine(
    format.timestamp(),
    format.json()
  ),
  transports: [
    new transports.File({ filename: 'security.log' }),
    new transports.Console()
  ]
});

export function logSecurityEvent(event: {
  type: string;
  severity: 'low' | 'medium' | 'high' | 'critical';
  details: Record<string, any>;
}) {
  securityLogger.log({
    level: event.severity === 'critical' ? 'error' : 'warn',
    message: event.type,
    ...event.details,
    timestamp: new Date().toISOString()
  });
}

// Usage in middleware or Server Actions
export function detectSuspiciousActivity(request: NextRequest) {
  const suspiciousPatterns = [
    { header: 'x-middleware-subrequest', severity: 'high' as const },
    { header: 'x-invoke-path', severity: 'high' as const },
    { header: 'x-middleware-invoke', severity: 'high' as const }
  ];
  
  for (const pattern of suspiciousPatterns) {
    if (request.headers.has(pattern.header)) {
      logSecurityEvent({
        type: 'suspicious_header_detected',
        severity: pattern.severity,
        details: {
          header: pattern.header,
          value: request.headers.get(pattern.header),
          ip: request.ip,
          url: request.url,
          userAgent: request.headers.get('user-agent')
        }
      });
      
      return true;
    }
  }
  
  return false;
}

Security Configuration Checklist

Based on production security audits, implement these configurations:

next.config.js Security Hardening

// 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: 'X-XSS-Protection',
    value: '1; mode=block'
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin'
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()'
  }
];

module.exports = {
  // Apply security headers to all routes
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders,
      },
    ];
  },
  
  // Disable source maps in production
  productionBrowserSourceMaps: false,
  
  // Enable strict mode for better error detection
  reactStrictMode: true,
  
  // Configure allowed image domains
  images: {
    domains: ['trusted-cdn.com'],
    dangerouslyAllowSVG: false,
    contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
  },
  
  // Disable X-Powered-By header
  poweredByHeader: false,
  
  // Enable HTTP compression
  compress: true,
};

Environment Variable Security

# .env.local
# Server-only variables (never exposed to client)
DATABASE_URL="postgresql://..."
API_SECRET_KEY="..."
JWT_SECRET="..."

# Client-accessible variables (use NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_ANALYTICS_ID="..."
// lib/env.ts - Type-safe environment validation
import { z } from 'zod';

const serverSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_SECRET_KEY: z.string().min(32),
  JWT_SECRET: z.string().min(32),
  NODE_ENV: z.enum(['development', 'test', 'production'])
});

const clientSchema = z.object({
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_ANALYTICS_ID: z.string().optional()
});

// Validate on application startup
const serverEnv = serverSchema.parse(process.env);
const clientEnv = clientSchema.parse({
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID
});

export { serverEnv, clientEnv };

Comprehensive Security Audit Checklist

Authentication & Authorization

  • Authentication logic exists at both middleware AND route level
  • Server Actions validate session before executing privileged operations
  • API routes implement their own auth checks (don't rely on middleware alone)
  • Role-based access control is enforced at the data access layer
  • Session tokens use secure, httpOnly cookies with SameSite=Strict
  • JWT secrets are cryptographically random (minimum 256 bits)
  • Password reset tokens expire within 1 hour
  • Multi-factor authentication is available for sensitive operations

Input Validation

  • All Server Actions validate inputs with Zod, Yup, or similar schema validators
  • File uploads are validated for type, size, and content (not just extension)
  • Query parameters are sanitized before database queries (use parameterized queries)
  • Form data is validated server-side (never trust client validation)
  • URL parameters in redirects are validated against allow-lists
  • User-generated content is sanitized before rendering (XSS prevention)
  • SQL injection prevention through ORM or prepared statements
  • Command injection prevention (never pass user input to shell commands)

Dependency Management

  • Next.js is on latest stable patched version (check Next.js releases)
  • React is on latest patched version
  • npm audit runs in CI/CD pipeline with failure on high/critical vulnerabilities
  • Dependabot or Renovate is configured for automated security updates
  • Lock files (package-lock.json) are committed and verified in CI
  • Dependency review in pull requests
  • Regular security updates scheduled (at least monthly)

Configuration Security

  • Security headers configured in next.config.js (CSP, HSTS, X-Frame-Options, etc.)
  • Source maps disabled in production (productionBrowserSourceMaps: false)
  • Environment variables properly scoped (NEXT_PUBLIC_ only for client-safe values)
  • CORS configured restrictively for API routes
  • Image optimization domains are allow-listed
  • SVG upload disabled or heavily sanitized
  • X-Powered-By header disabled

Deployment Security

  • Internal headers stripped at edge/reverse proxy (x-middleware-subrequest, etc.)
  • Rate limiting implemented for Server Actions and API routes
  • Logging captures security-relevant events (authentication failures, suspicious headers)
  • Monitoring alerts on suspicious patterns (repeated 401s, header manipulation)
  • HTTPS enforced (HSTS with preload)
  • TLS 1.2+ minimum version
  • Regular security scanning (OWASP ZAP, Burp Suite)

React Server Components (if used)

  • Server Components don't leak sensitive data to client (check serialized props)
  • Server Actions exported from files with 'use server' directive
  • Client Components marked with 'use client' directive
  • Serialized data doesn't include credentials, tokens, or internal IDs
  • Database query results filtered to public fields before passing to Client Components
  • No sensitive environment variables accessed in Client Components

Data Protection

  • Passwords hashed with bcrypt or Argon2 (never MD5/SHA1)
  • Sensitive data encrypted at rest
  • PII (Personally Identifiable Information) handling complies with GDPR/CCPA
  • Database connections use TLS
  • API keys rotated regularly
  • Secrets stored in secure vault (not in code or environment variables in CI)

Keeping Up with Security Updates

Official Sources to Monitor

  1. Next.js Security Advisories

    • GitHub Security Advisories
    • Subscribe to releases: Watch → Custom → Releases on GitHub
    • RSS feed: https://github.com/vercel/next.js/releases.atom
  2. React Security

  3. CVE Databases

  4. npm Security

Automated Monitoring Setup

# .github/workflows/security-audit.yml
name: Security Audit

on:
  schedule:
    - cron: '0 0 * * 1'  # Weekly on Monday
  pull_request:
  push:
    branches: [main]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run npm audit
        run: npm audit --audit-level=moderate
        
      - name: Check for outdated dependencies
        run: npm outdated
        
      - name: Dependency Review
        uses: actions/dependency-review-action@v3
        if: github.event_name == 'pull_request'

Real-World Incident Response

Based on actual security incidents, here's a response playbook:

Immediate Actions (First 24 Hours)

  1. Assess the vulnerability

    • Check your Next.js and React versions against affected ranges
    • Review CVE severity and exploitability
    • Determine if your application uses affected features
  2. Deploy emergency patches

    • Apply vendor-provided patches immediately
    • If patches unavailable, implement workarounds (WAF rules, header filtering)
    • Test patches in staging before production
  3. Check for exploitation

    • Review access logs for suspicious patterns
    • Check for unexpected Server Action requests
    • Monitor for unusual outbound connections (data exfiltration)
    • Review authentication logs for unauthorized access
  4. Contain potential damage

    • Rotate API keys and secrets if compromise suspected
    • Force password resets for affected users
    • Enable enhanced logging and monitoring

Investigation Phase (Days 2-7)

  1. Forensic analysis

    • Preserve logs and system snapshots
    • Analyze traffic patterns before/during/after vulnerability disclosure
    • Check database audit logs for unauthorized queries
    • Review file system for web shells or malicious uploads
  2. Impact assessment

    • Identify what data could have been accessed
    • Determine which users might be affected
    • Calculate potential GDPR/CCPA implications
  3. Communication

    • Notify security team and management
    • Prepare customer communication if data breach occurred
    • Document timeline and actions taken

Long-term Remediation (Weeks 2-4)

  1. Architecture review

    • Implement defense-in-depth patterns from this guide
    • Add security testing to CI/CD pipeline
    • Schedule regular security audits
  2. Process improvements

    • Establish faster patch deployment procedures
    • Create security incident playbooks
    • Train development team on secure coding practices

Additional Resources

Security Tools

  • OWASP ZAP - Free web application security scanner
  • npm audit - Built-in dependency vulnerability checker
  • Snyk - Automated vulnerability scanning for dependencies
  • Semgrep - Static analysis security testing (SAST)
  • SonarQube - Code quality and security analysis

Learning Resources

Community

Conclusion

Next.js and React security is not a one-time task—it's an ongoing process. The vulnerabilities discussed in this guide (CVE-2022-24999, CVE-2024-46982, CVE-2023-46298) demonstrate common patterns:

  1. Trust boundary violations - Assuming internal headers or protocols can't be manipulated
  2. Input validation gaps - Not validating user-controlled data before processing
  3. Single-layer security - Relying on one security mechanism (like middleware) without defense-in-depth

By implementing the patterns and practices in this guide, you can build Next.js applications that are resilient to these classes of vulnerabilities:

  • Layered security at edge, middleware, route, and data access levels
  • Comprehensive input validation using schema validators like Zod
  • Proper authorization checks at every security-sensitive operation
  • Monitoring and logging to detect exploitation attempts
  • Regular updates through automated dependency management

Security is not a feature you add later—it's the foundation everything else is built on. Stay informed, patch promptly, and always assume that any user input could be malicious.

Frequently Asked Questions

Q: How do I check if my Next.js app is vulnerable to known CVEs?

Check your package.json for Next.js and React versions, then compare against CVE documentation. Run npm audit to check for known vulnerabilities. Use the GitHub Advisory Database to search for your specific versions.

Q: Are Next.js applications on Vercel automatically protected?

Vercel provides some protections (header stripping, DDoS mitigation), but you still need to:

  • Keep dependencies updated
  • Implement proper authorization
  • Validate all inputs
  • Follow security best practices

Deployment platform security is one layer—application-level security is your responsibility.

Q: Should I disable Server Actions entirely?

No. Server Actions are secure when used correctly:

  • Always validate inputs with schema validators
  • Check authentication and authorization
  • Use the 'use server' directive properly
  • Never trust client-provided data

The issues are in implementation, not the feature itself.

Q: How often should I update Next.js and React?

For security updates: immediately after testing in staging. For feature updates: follow a regular schedule (e.g., monthly) but prioritize security patches. Subscribe to security advisories to know when critical updates are released.

Q: What's the performance impact of adding all these security measures?

Minimal. Input validation adds microseconds, authorization checks are necessary anyway, and security headers have no runtime cost. The real cost is development time—but it's far less than dealing with a security breach.

Q: Can I use a WAF to protect against all Next.js vulnerabilities?

No. WAFs provide one layer of defense but can't catch everything:

  • Application logic flaws require code fixes
  • Some exploits use legitimate-looking requests
  • WAF rules need constant updates

Use WAFs as part of defense-in-depth, not as your only protection.

Q: Where can I report a Next.js security vulnerability?

Report to Vercel's security team: Next.js Security Policy. Do NOT disclose publicly until they've had time to patch.

Q: How do I test my Next.js app for security issues?

  1. Run npm audit for dependency vulnerabilities
  2. Use OWASP ZAP or Burp Suite for penetration testing
  3. Implement security unit tests for authorization logic
  4. Conduct regular code reviews focused on security
  5. Consider hiring professional security auditors for critical applications

Q: What logs should I monitor for security events?

Monitor for:

  • Failed authentication attempts (potential brute force)
  • Requests with suspicious headers (x-middleware-subrequest, etc.)
  • Unusual patterns of Server Action calls
  • 403/401 errors from the same IP
  • Unexpected changes to user roles or permissions
  • File uploads with suspicious content types

Implement alerting for patterns that indicate active exploitation attempts.