Express.js Security Checklist for Heroku

Express.js is minimalist - you must add security features yourself. This checklist ensures your Node.js/Express app is production-ready on Heroku.

Expedited WAF for Express

Expedited WAF adds network level request filtering, preventing malicious attacks from ever touching your application. This enables true defense in depth for your Express 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 Express app on Heroku

New Security Features for Your Express App

These capabilities aren’t available out of the box with Express, but Expedited WAF adds them instantly:

Core Security Configuration

  • Use Helmet for security headers

    const helmet = require('helmet');
    app.use(helmet());
    
    // Or configure specific headers
    app.use(helmet({
      contentSecurityPolicy: {
        directives: {
          defaultSrc: ["'self'"],
          styleSrc: ["'self'", "'unsafe-inline'"],
          scriptSrc: ["'self'"],
          imgSrc: ["'self'", "data:", "https:"]
        }
      },
      hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true
      }
    }));
    
  • Disable X-Powered-By header

    app.disable('x-powered-by');
    
  • Set secure session configuration

    const session = require('express-session');
    
    app.use(session({
      secret: process.env.SESSION_SECRET,
      resave: false,
      saveUninitialized: false,
      cookie: {
        secure: process.env.NODE_ENV === 'production', // HTTPS only
        httpOnly: true,
        maxAge: 1000 * 60 * 60 * 24, // 24 hours
        sameSite: 'strict'
      }
    }));
    
  • Enable trust proxy for Heroku

    app.set('trust proxy', 1); // Trust first proxy (Heroku router)
    

HTTPS & SSL

  • Enforce HTTPS in production

    if (process.env.NODE_ENV === 'production') {
      app.use((req, res, next) => {
        if (req.header('x-forwarded-proto') !== 'https') {
          res.redirect(`https://${req.header('host')}${req.url}`);
        } else {
          next();
        }
      });
    }
    
  • Configure HSTS

Input Validation & Sanitization

  • Validate all input

    const { body, validationResult } = require('express-validator');
    
    app.post('/user', [
      body('email').isEmail().normalizeEmail(),
      body('password').isLength({ min: 12 }),
      body('name').trim().escape()
    ], (req, res) => {
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
      }
      // Process valid data
    });
    
  • Sanitize user input

    const validator = require('validator');
    
    const cleanInput = validator.escape(userInput);
    
  • Prevent NoSQL injection

    const mongoSanitize = require('express-mongo-sanitize');
    app.use(mongoSanitize());
    
  • Limit request body size

    app.use(express.json({ limit: '10kb' }));
    app.use(express.urlencoded({ extended: true, limit: '10kb' }));
    

CSRF Protection

  • Implement CSRF protection

    const csrf = require('csurf');
    const csrfProtection = csrf({ cookie: true });
    
    app.use(csrfProtection);
    
    app.get('/form', (req, res) => {
      res.render('form', { csrfToken: req.csrfToken() });
    });
    
  • Add CSRF token to forms

    <input type="hidden" name="_csrf" value="{{csrfToken}}">
    

Rate Limiting & DDoS Protection

  • Implement rate limiting

    const rateLimit = require('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'
    });
    
    app.use('/api/', limiter);
    
    // Stricter limits for auth endpoints
    const authLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 5,
      skipSuccessfulRequests: true
    });
    
    app.post('/login', authLimiter, loginHandler);
    
  • Prevent brute force attacks

    const ExpressBrute = require('express-brute');
    const store = new ExpressBrute.MemoryStore();
    const bruteforce = new ExpressBrute(store);
    
    app.post('/login', bruteforce.prevent, loginHandler);
    
  • Configure Heroku DDoS protection

Authentication & Session Security

  • Use passport.js for authentication

    const passport = require('passport');
    const LocalStrategy = require('passport-local').Strategy;
    
    passport.use(new LocalStrategy(
      function(username, password, done) {
        // Verify credentials
      }
    ));
    
    app.use(passport.initialize());
    app.use(passport.session());
    
  • Hash passwords with bcrypt

    const bcrypt = require('bcrypt');
    const saltRounds = 12;
    
    // Hashing
    const hashedPassword = await bcrypt.hash(password, saltRounds);
    
    // Comparing
    const match = await bcrypt.compare(password, hashedPassword);
    
  • Implement JWT for APIs

    const jwt = require('jsonwebtoken');
    
    // Generate token
    const token = jwt.sign(
      { userId: user.id },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    // Verify token middleware
    function authenticateToken(req, res, next) {
      const token = req.headers['authorization']?.split(' ')[1];
    
      if (!token) return res.sendStatus(401);
    
      jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) return res.sendStatus(403);
        req.user = user;
        next();
      });
    }
    

Database Security

  • Use environment variables for DB connection

    const mongoose = require('mongoose');
    
    mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      ssl: true
    });
    
  • Use parameterized queries

    // Good - parameterized
    User.findOne({ email: userEmail });
    
    // Bad - injection risk
    db.query(`SELECT * FROM users WHERE email = '${userEmail}'`);
    
  • Enable Mongoose schema validation

    const userSchema = new mongoose.Schema({
      email: {
        type: String,
        required: true,
        unique: true,
        lowercase: true,
        trim: true
      },
      password: {
        type: String,
        required: true,
        minlength: 12
      }
    });
    

CORS Configuration

  • Configure CORS properly

    const cors = require('cors');
    
    const corsOptions = {
      origin: process.env.ALLOWED_ORIGINS?.split(',') || 'https://yourdomain.com',
      credentials: true,
      optionsSuccessStatus: 200
    };
    
    app.use(cors(corsOptions));
    
  • Never use CORS wildcard in production

    // Bad - allows all origins
    app.use(cors());
    
    // Good - specific origins
    app.use(cors({ origin: 'https://yourdomain.com' }));
    

File Uploads

  • Validate file uploads

    const multer = require('multer');
    
    const upload = multer({
      limits: {
        fileSize: 5 * 1024 * 1024 // 5MB
      },
      fileFilter: (req, file, cb) => {
        const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
        if (allowedTypes.includes(file.mimetype)) {
          cb(null, true);
        } else {
          cb(new Error('Invalid file type'));
        }
      }
    });
    
    app.post('/upload', upload.single('avatar'), uploadHandler);
    
  • Store uploads on S3, not Heroku filesystem

    const aws = require('aws-sdk');
    const multerS3 = require('multer-s3');
    
    const s3 = new aws.S3({
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
    });
    
    const upload = multer({
      storage: multerS3({
        s3: s3,
        bucket: process.env.S3_BUCKET,
        metadata: function (req, file, cb) {
          cb(null, {fieldName: file.fieldname});
        },
        key: function (req, file, cb) {
          cb(null, Date.now().toString() + '-' + file.originalname)
        }
      })
    });
    

Error Handling

  • Use centralized error handling

    // Error handling middleware (must be last)
    app.use((err, req, res, next) => {
      console.error(err.stack);
    
      // Don't expose stack traces in production
      if (process.env.NODE_ENV === 'production') {
        res.status(err.status || 500).json({
          error: 'Internal server error'
        });
      } else {
        res.status(err.status || 500).json({
          error: err.message,
          stack: err.stack
        });
      }
    });
    
  • Handle async errors

    // Wrapper for async route handlers
    const asyncHandler = fn => (req, res, next) => {
      Promise.resolve(fn(req, res, next)).catch(next);
    };
    
    app.get('/users', asyncHandler(async (req, res) => {
      const users = await User.find();
      res.json(users);
    }));
    

Environment Variables & Secrets

  • Use environment variables

    require('dotenv').config();
    
    const config = {
      port: process.env.PORT || 3000,
      dbUri: process.env.DATABASE_URL,
      jwtSecret: process.env.JWT_SECRET,
      sessionSecret: process.env.SESSION_SECRET
    };
    
  • Store secrets in Heroku config

    heroku config:set JWT_SECRET='...'
    heroku config:set SESSION_SECRET='...'
    heroku config:set DATABASE_URL='...'
    
  • Never commit .env files

    • Add to .gitignore
    • Use .env.example for template

Logging & Monitoring

  • Implement structured logging

    const winston = require('winston');
    
    const logger = winston.createLogger({
      level: 'info',
      format: winston.format.json(),
      transports: [
        new winston.transports.Console()
      ]
    });
    
    app.use((req, res, next) => {
      logger.info(`${req.method} ${req.path}`);
      next();
    });
    
  • Don’t log sensitive data

    • Passwords, tokens, API keys
    • Credit card numbers
    • Personal information
  • Use Morgan for HTTP logging

    const morgan = require('morgan');
    app.use(morgan('combined'));
    

Dependency Security

  • Audit npm packages regularly

    npm audit
    npm audit fix
    
  • Keep dependencies updated

    npm outdated
    npm update
    
  • Use npm lockfile

    • Commit package-lock.json
    • Use npm ci in production
  • Remove unused dependencies

    npm prune --production
    

Heroku Deployment

  • Create Procfile

    web: node server.js
    
  • Specify Node.js version

    // package.json
    {
      "engines": {
        "node": "18.x",
        "npm": "9.x"
      }
    }
    
  • Use production mode

    heroku config:set NODE_ENV=production
    
  • Configure process manager (optional)

    web: node_modules/.bin/pm2 start server.js -i max --no-daemon
    
  • Enable compression

    const compression = require('compression');
    app.use(compression());
    

Additional Security Middleware

  • Install essential security packages

    npm install helmet express-rate-limit express-mongo-sanitize hpp express-validator
    
  • Prevent parameter pollution

    const hpp = require('hpp');
    app.use(hpp());
    

Testing & Pre-Deployment

  • Test security headers

    • Use securityheaders.com
    • Verify CSP, HSTS, X-Frame-Options
  • Run security linters

    npm install --save-dev eslint-plugin-security
    
  • Test on Heroku staging

    heroku create myapp-staging
    git push staging main
    

Essential Express.js Security Packages

Additional Resources