Next.js Security Checklist for Heroku

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.

Expedited WAF protects your Next.js app on Heroku

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:

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 ci in 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

Additional Resources