← Back to Articles

Understanding Next.js Security Best Practices and Common Vulnerabilities

Code

Security is paramount when building web applications, especially with frameworks like Next.js that handle both client and server-side code. While Next.js provides many built-in security features, there are still important considerations and common vulnerabilities you need to be aware of. In this article, I'll cover the essential security best practices and common pitfalls to avoid.

Why Next.js security matters

Next.js applications often handle sensitive data, user authentication, and server-side operations. A security vulnerability could lead to:

  • Data breaches and privacy violations
  • Unauthorized access to user accounts
  • Server compromise and downtime
  • Loss of user trust and business reputation

Server-Side Request Forgery (SSRF) protection

SSRF attacks occur when an attacker tricks your application into making requests to internal services or external systems. In Next.js, this commonly happens with:

  • Image optimization endpoints
  • API routes that proxy requests
  • Server components that fetch data

Prevention strategies:

  1. Validate URLs: Always validate and sanitize URLs before making requests
  2. Use allowlists: Only allow requests to trusted domains
  3. Localhost protection: Block requests to localhost and internal IP ranges
// api/image-proxy.js
import { NextResponse } from 'next/server';

const ALLOWED_DOMAINS = ['trusted-cdn.com', 'my-domain.com'];

export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const imageUrl = searchParams.get('url');

  try {
    const url = new URL(imageUrl);

    // Check if domain is allowed
    if (!ALLOWED_DOMAINS.includes(url.hostname)) {
      return NextResponse.json({ error: 'Domain not allowed' }, { status: 403 });
    }

    // Prevent localhost access
    if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
      return NextResponse.json({ error: 'Localhost access denied' }, { status: 403 });
    }

    const response = await fetch(imageUrl);
    const buffer = await response.arrayBuffer();

    return new NextResponse(buffer, {
      headers: { 'Content-Type': response.headers.get('content-type') }
    });
  } catch (error) {
    return NextResponse.json({ error: 'Invalid URL' }, { status: 400 });
  }
}

Cross-Site Scripting (XSS) prevention

XSS attacks inject malicious scripts into your application. Next.js provides some protection, but you need to be careful with:

  • User-generated content
  • Dynamic HTML rendering
  • Inline event handlers

Best practices:

  1. Sanitize user input: Use libraries like DOMPurify for HTML content
  2. Content Security Policy: Implement strict CSP headers
  3. Avoid dangerouslySetInnerHTML: Use safer alternatives when possible

Authentication and authorization

Secure user authentication is crucial:

JWT security:

  • Use strong secrets for signing
  • Implement proper token expiration
  • Validate tokens on every request

Session management:

  • Use secure, httpOnly cookies
  • Implement proper session invalidation
  • Protect against session fixation attacks

API security

Protect your API routes:

Rate limiting:

// middleware.js
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP, please try again later.'
});

export default limiter;

Input validation:

import { z } from 'zod';

const userSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2).max(100)
});

export async function POST(request) {
  try {
    const body = await request.json();
    const validatedData = userSchema.parse(body);

    // Process validated data
    return NextResponse.json({ success: true });
  } catch (error) {
    return NextResponse.json({ error: 'Invalid input' }, { status: 400 });
  }
}

Environment variable security

Protect sensitive configuration:

  1. Never commit secrets: Use .env files (add to .gitignore)
  2. Use different secrets for each environment
  3. Validate environment variables on startup
// lib/config.js
if (!process.env.DATABASE_URL) {
  throw new Error('DATABASE_URL environment variable is required');
}

if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) {
  throw new Error('JWT_SECRET must be at least 32 characters long');
}

Dependency security

Keep your dependencies secure:

Regular updates:

npm audit
npm audit fix

Use tools like:

  • Snyk for vulnerability scanning
  • Dependabot for automated updates
  • npm audit for built-in security checks

Server-side security

Protect your server environment:

HTTPS enforcement:

// middleware.js
export function middleware(request) {
  if (process.env.NODE_ENV === 'production' && !request.url.startsWith('https://')) {
    return NextResponse.redirect(new URL(request.url.replace('http://', 'https://')));
  }
}

Security headers:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { 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=()' }
        ]
      }
    ];
  }
};

Database security

Secure your database connections:

Connection pooling:

// lib/db.js
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
  max: 20, // Maximum number of connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

export default pool;

Prepared statements:

// Avoid SQL injection
const query = 'SELECT * FROM users WHERE email = $1';
const values = [userEmail];
const result = await pool.query(query, values);

Regular security audits

Perform regular security assessments:

Code reviews: Always review security implications in code reviews Penetration testing: Hire professionals to test your security Dependency scanning: Use tools like Snyk or npm audit Compliance: Meet industry standards (GDPR, HIPAA, etc.)

Incident response plan

Be prepared for security incidents:

  1. Detection: Monitor for suspicious activity
  2. Containment: Isolate affected systems
  3. Eradication: Remove the threat
  4. Recovery: Restore systems and data
  5. Lessons learned: Update practices to prevent future incidents

The bottom line

Next.js security requires vigilance and best practices. Focus on input validation, proper authentication, secure configurations, and regular updates. Security isn't a one-time task—it's an ongoing commitment to protecting your users and your business. Stay informed about security developments and always err on the side of caution when handling sensitive data or operations.

About the author

Rafael De Paz

Full Stack Developer

Passionate full-stack developer specializing in building high-quality web applications and responsive sites. Expert in robust data handling, leveraging modern frameworks, cloud technologies, and AI tools to deliver scalable, high-performance solutions that drive user engagement and business growth. I harness AI technologies to accelerate development, testing, and debugging workflows.

Tags:

Share: