Next.js is a powerful framework for building modern web applications, but even the best tools can leave your project vulnerable if security is overlooked. From misconfigured settings to overlooked vulnerabilities, common mistakes can expose your application to attacks. In this article, we explore the most frequent security pitfalls in Next.js applications and provide actionable tips to safeguard your project.

1. Exposing Sensitive Environment Variables

Mistake: Including sensitive environment variables (e.g., database credentials, API keys) in the client-side code.

Example:

export const API_KEY = process.env.API_KEY; // Exposed in client bundle
  
Mitigation:

Use the NEXT_PUBLIC_ prefix for variables that should be exposed to the client. Keep sensitive data in server-side-only environment variables.


export async function fetchData() {
  const apiKey = process.env.API_KEY;
  const res = await fetch(`https://api.example.com?key=${apiKey}`);
  return res.json();
}
  

2. Insecure API Routes

Mistake: Not validating input or implementing proper authentication on API routes.

Example:

// /pages/api/update-user.js
export default async function handler(req, res) {
  const { userId, newName } = req.body;
  // Directly updates user without authentication or validation
  await db.users.update({ id: userId }, { name: newName });
  res.status(200).json({ success: true });
}
  
Mitigation:

Validate inputs using libraries like Joi or Zod. Require authentication/authorization for sensitive operations.


import { getSession } from 'next-auth/react';
import Joi from 'joi';

export default async function handler(req, res) {
  const session = await getSession({ req });
  if (!session) return res.status(401).json({ 'error': 'Unauthorized' });

  const schema = Joi.object({
    userId: Joi.string().required(),
    newName: Joi.string().min(3).max(50).required(),
  });

  const { error } = schema.validate(req.body);
  if (error) return res.status(400).json({ 'error': error.message });

  await db.users.update({ id: req.body.userId }, { name: req.body.newName });
  res.status(200).json({ success: true });
}
  

3. Improper Access Control in `getServerSideProps`

Mistake: Fetching data in `getServerSideProps` without validating the user's permissions.

Example:

export async function getServerSideProps(context) {
  const data = await fetch(`https://api.example.com/data?id=${context.query.id}`);
  const json = await data.json();
  return { props: { data: json } };
}
  
Mitigation:

Verify the user's session and permissions before returning data.


import { getSession } from 'next-auth/react';

export async function getServerSideProps(context) {
  const session = await getSession(context);
  if (!session || !session.user.isAdmin) {
    return { redirect: { destination: '/login', permanent: false } };
  }

  const data = await fetch(`https://api.example.com/data?id=${context.query.id}`);
  const json = await data.json();

  return { props: { data: json } };
}
  

4. Cross-Site Scripting (XSS)

Mistake: Rendering unescaped user-generated content.

Example:

export default function Page({ content }) {
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}
  
Mitigation:

Avoid dangerouslySetInnerHTML unless absolutely necessary. Sanitize input with libraries like DOMPurify.


import DOMPurify from 'dompurify';

export default function Page({ content }) {
  const sanitizedContent = DOMPurify.sanitize(content);
  return <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />;
}
  

5. Insecure Use of Third-Party Packages

Mistake: Installing packages without checking their security vulnerabilities or updates.

Example:

// Using outdated or abandoned libraries
  
Mitigation:

Regularly audit dependencies using npm audit or tools like Snyk. Update dependencies frequently and remove unused packages.


npm audit fix
  

6. Insecure Static File Serving

Mistake: Allowing unrestricted access to sensitive files in the /public folder.

Example:

// Storing a .env file in the /public directory
  
Mitigation:

Only include non-sensitive, public files in the /public directory. Ensure sensitive files are stored securely outside the /public directory.

7. Inadequate CSRF Protection

Mistake: Not protecting API routes from Cross-Site Request Forgery (CSRF) attacks.

Example:

export default async function handler(req, res) {
  // No CSRF protection
  const { action } = req.body;
  await performAction(action);
  res.status(200).json({ success: true });
}
  
Mitigation:

Use CSRF tokens for sensitive operations, especially when using authenticated APIs. Implement libraries like next-csrf.

8. Server-Side Request Forgery (SSRF)

Mistake: Allowing user-controlled input in server-side HTTP requests without validation.

Example:

export default async function handler(req, res) {
  const { url } = req.body;
  const data = await fetch(url); // User can request internal endpoints
  res.status(200).json(await data.json());
}
  
Mitigation:

Validate URLs before making requests. Use libraries like url for validation or limit the allowed domains.


import { URL } from 'url';

export default async function handler(req, res) {
  const { url } = req.body;
  try {
    const parsedUrl = new URL(url);
    if (!['https://allowed-domain.com'].includes(parsedUrl.origin)) {
      throw new Error('Invalid URL origin');
    }
  } catch (error) {
    return res.status(400).json({ 'error': 'Invalid URL' });
  }

  const data = await fetch(url);
  res.status(200).json(await data.json());
}