Next.js combines React frontend with Node.js backend - secure both sides for production deployment.
Expedited WAF for Next.js
Expedited WAF adds network level request filtering, preventing malicious attacks from ever touching your application. This enables true defense in depth for your Next.js app—even if your framework has a vulnerability or is improperly configured, Expedited WAF dynamically blocks threats before they reach your code.
From experience, we’ve seen many applications where attacks technically didn’t succeed, but burned through so many server resources that the application went down anyway. By filtering malicious requests at the network edge, Expedited WAF protects both your security and your availability.
New Security Features for Your Next.js App
These capabilities aren’t available out of the box with Next.js, but Expedited WAF adds them instantly:
- Block IP Addresses - Stop malicious actors by IP or CIDR range
- Block User Agents - Filter out bad bots and scrapers
- Block by Geolocation - Restrict traffic by country or region
- DDoS Protection - Stop attacks with CAPTCHA challenges
- Block Anonymous Proxies - Prevent traffic from VPNs and proxy networks
Next.js Configuration
-
Configure security headers in next.config.js
// next.config.js module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'X-XSS-Protection', value: '1; mode=block' }, { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }, { key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" } ] } ] } } -
Disable X-Powered-By header
// next.config.js module.exports = { poweredByHeader: false } -
Configure environment variables properly
// Only NEXT_PUBLIC_* variables are exposed to browser // Keep secrets without NEXT_PUBLIC_ prefix const apiKey = process.env.API_KEY; // Server-side only const publicKey = process.env.NEXT_PUBLIC_KEY; // Client-side accessible
API Routes Security
-
Validate all API route inputs
// pages/api/user.js import { z } from 'zod'; const schema = z.object({ email: z.string().email(), name: z.string().min(1).max(100) }); export default async function handler(req, res) { try { const validated = schema.parse(req.body); // Process validated data } catch (error) { return res.status(400).json({ error: 'Invalid input' }); } } -
Implement authentication for API routes
// lib/auth.js import { verify } from 'jsonwebtoken'; export function withAuth(handler) { return async (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Unauthorized' }); } try { const decoded = verify(token, process.env.JWT_SECRET); req.user = decoded; return handler(req, res); } catch (error) { return res.status(401).json({ error: 'Invalid token' }); } }; } // pages/api/protected.js export default withAuth(async function handler(req, res) { // Protected route logic }); -
Implement rate limiting
import rateLimit from 'express-rate-limit'; import slowDown from 'express-slow-down'; const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }); export default limiter(async function handler(req, res) { // API logic }); -
Use HTTP method validation
export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } // POST logic }
React/Frontend Security
-
Sanitize user-generated content
import DOMPurify from 'isomorphic-dompurify'; function SafeHTML({ html }) { return ( <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} /> ); } -
Avoid dangerouslySetInnerHTML when possible
- Use regular JSX instead
- Only use with sanitized content
-
Implement CSP for inline scripts
- Use nonce-based CSP for Next.js
- Configure in next.config.js
-
Validate props and user input
import PropTypes from 'prop-types'; function UserProfile({ userId }) { // Validate userId is a number if (typeof userId !== 'number') { return null; } // Render component } UserProfile.propTypes = { userId: PropTypes.number.isRequired };
Authentication & Session Management
-
Use NextAuth.js for authentication
npm install next-auth// pages/api/auth/[...nextauth].js import NextAuth from 'next-auth'; import Providers from 'next-auth/providers'; export default NextAuth({ providers: [ Providers.Credentials({ async authorize(credentials) { // Validate credentials return user; } }) ], session: { jwt: true, maxAge: 30 * 24 * 60 * 60, // 30 days }, cookies: { sessionToken: { name: `__Secure-next-auth.session-token`, options: { httpOnly: true, sameSite: 'lax', path: '/', secure: process.env.NODE_ENV === 'production' } } } }); -
Protect pages with authentication
import { getSession } from 'next-auth/react'; export async function getServerSideProps(context) { const session = await getSession(context); if (!session) { return { redirect: { destination: '/login', permanent: false } }; } return { props: { session } }; }
CSRF Protection
- Implement CSRF tokens for forms
import { getCsrfToken } from 'next-auth/react'; export async function getServerSideProps(context) { const csrfToken = await getCsrfToken(context); return { props: { csrfToken } }; } function Form({ csrfToken }) { return ( <form method="post"> <input type="hidden" name="csrfToken" value={csrfToken} /> {/* Other fields */} </form> ); }
Environment & Secrets
-
Separate client and server environment variables
# .env.local DATABASE_URL=... # Server-side only JWT_SECRET=... # Server-side only NEXT_PUBLIC_API_URL=... # Client-side accessible -
Store secrets in Heroku config
heroku config:set DATABASE_URL='...' heroku config:set JWT_SECRET='...' heroku config:set NEXT_PUBLIC_API_URL='...' -
Never expose server secrets to client
- Don’t use NEXT_PUBLIC_ prefix for secrets
- Verify in browser console what’s exposed
Build & Deployment
-
Configure for production build
// package.json { "scripts": { "build": "next build", "start": "next start -p $PORT" } } -
Create Procfile for Heroku
web: npm run start -
Enable SWC minification
// next.config.js module.exports = { swcMinify: true } -
Specify Node.js version
// package.json { "engines": { "node": "18.x" } }
Image Optimization Security
-
Configure allowed image domains
// next.config.js module.exports = { images: { domains: ['yourdomain.com', 'cdn.yourdomain.com'], // Don't allow arbitrary external images } } -
Use Next.js Image component
import Image from 'next/image'; function Avatar({ src }) { return <Image src={src} width={50} height={50} alt="Avatar" />; }
Database & External Services
-
Use environment variables for connections
import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient({ datasources: { db: { url: process.env.DATABASE_URL } } }); -
Implement connection pooling
- Configure in DATABASE_URL
- Use Prisma or similar ORM
-
Sanitize database queries
- Use parameterized queries
- Use ORMs (Prisma, TypeORM)
Dependencies & Updates
-
Audit dependencies regularly
npm audit npm audit fix -
Keep Next.js updated
npm install next@latest react@latest react-dom@latest -
Use lockfile
- Commit package-lock.json
- Use
npm ciin Heroku
Testing & Pre-Deployment
-
Test build locally
npm run build npm run start -
Check bundle size
npm run build # Review .next/analyze output -
Test on Heroku staging
heroku create myapp-staging git push staging main -
Verify security headers
- Use securityheaders.com
- Check browser dev tools