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.
New Security Features for Your Express App
These capabilities aren’t available out of the box with Express, 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
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
- Included in Helmet
- How to Force HTTPS on Heroku
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 ciin 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
helmet- Security headersexpress-rate-limit- Rate limitingexpress-validator- Input validationexpress-mongo-sanitize- NoSQL injection preventionhpp- HTTP parameter pollution protectioncors- CORS configurationcsurf- CSRF protectionbcrypt- Password hashingjsonwebtoken- JWT authenticationpassport- Authentication strategies