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.
// 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());
}