Express Security Checklist
Introduction
When building web applications with Express.js, security should be a top priority. Even simple applications can be vulnerable to various attacks if proper security measures aren't implemented. This guide covers essential security practices for Express applications, helping you protect your server, users, and data from common threats.
Security isn't just for production applications—implementing these practices during development helps build good habits and prevents vulnerabilities from reaching your users. Let's explore the key security measures every Express application should implement.
Why Security Matters in Express
Express provides a flexible framework for building web applications, but this flexibility means you're responsible for implementing proper security measures. Common security vulnerabilities in Express applications include:
- Cross-Site Scripting (XSS) attacks
- SQL/NoSQL injection attacks
- Cross-Site Request Forgery (CSRF)
- Insecure dependencies
- Information exposure through error messages
- Denial of Service (DoS) attacks
By following this checklist, you can mitigate these risks and create more robust applications.
Essential Express Security Measures
1. Use Helmet to Set Security Headers
Helmet helps secure Express apps by setting various HTTP headers to prevent common attacks.
Installation:
npm install helmet
Implementation:
const express = require('express');
const helmet = require('helmet');
const app = express();
// Apply helmet middleware
app.use(helmet());
app.get('/', (req, res) => {
res.send('Hello, secure world!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Helmet sets multiple headers including:
Content-Security-Policy
to prevent XSS attacksX-XSS-Protection
to enable browser XSS filtersX-Frame-Options
to prevent clickjackingX-Content-Type-Options
to prevent MIME-sniffing- And several others
You can also configure specific headers:
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "example.com"],
},
},
xssFilter: true,
})
);
2. Prevent Cross-Site Request Forgery (CSRF)
CSRF attacks trick authenticated users into submitting unwanted requests. Use the csurf
middleware to protect against this:
npm install csurf
Implementation:
const express = require('express');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const app = express();
// We need cookie-parser middleware before CSRF
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
// Set up CSRF protection
const csrfProtection = csrf({ cookie: true });
// Apply CSRF protection to routes that need it
app.get('/form', csrfProtection, (req, res) => {
// Pass the CSRF token to the view
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// Process the form if CSRF token is valid
res.send('Form processed!');
});
In your form template (e.g., using EJS):
<form action="/process" method="POST">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<!-- other form fields -->
<button type="submit">Submit</button>
</form>
3. Validate and Sanitize Input with Express-Validator
Never trust user input. Validate and sanitize all data using express-validator:
npm install express-validator
Implementation:
const express = require('express');
const { body, validationResult } = require('express-validator');
const app = express();
app.use(express.json());
app.post(
'/user',
// Validation and sanitization rules
[
body('email').isEmail().normalizeEmail(),
body('username').trim().isLength({ min: 3 }),
body('password').isLength({ min: 8 }),
body('age').isNumeric().toInt(),
],
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process valid data
const { email, username, password, age } = req.body;
res.json({ message: 'User created successfully' });
}
);
4. Implement Rate Limiting
Protect your app from brute force attacks using express-rate-limit:
npm install express-rate-limit
Implementation:
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// Basic rate limiter: max 100 requests per IP in 15 minutes
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests, please try again after 15 minutes'
});
// Apply rate limiter to all requests
app.use(limiter);
// Create a specific limiter for login attempts
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 login attempts per hour
message: 'Too many login attempts, please try again after an hour'
});
app.post('/login', loginLimiter, (req, res) => {
// Handle login logic
});
5. Use Environment Variables for Sensitive Information
Never hardcode sensitive information. Use environment variables with dotenv:
npm install dotenv
Create a .env
file:
DB_CONNECTION=mongodb://localhost:27017/myapp
JWT_SECRET=your_super_secret_key
API_KEY=external_service_api_key
Implementation:
// At the very beginning of your main file
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const app = express();
// Use environment variables
mongoose.connect(process.env.DB_CONNECTION, {
useNewUrlParser: true,
useUnifiedTopology: true
});
app.get('/protected', (req, res) => {
// Use JWT secret from environment
const token = jwt.sign({ user: 'user123' }, process.env.JWT_SECRET);
res.json({ token });
});
Important: Add .env
to your .gitignore
file to prevent committing sensitive data:
# .gitignore
.env
.env.local
node_modules/
6. Set Up Proper Error Handling
Implement error handling that doesn't leak sensitive information:
const express = require('express');
const app = express();
// Regular routes here...
// Error handling middleware (should be after all routes)
app.use((err, req, res, next) => {
console.error(err.stack);
// Custom error response for different environments
if (process.env.NODE_ENV === 'production') {
res.status(500).json({
error: 'An unexpected error occurred'
});
} else {
// More detailed error in development
res.status(500).json({
error: err.message,
stack: err.stack
});
}
});
// Handle 404 errors
app.use((req, res, next) => {
res.status(404).json({ error: 'Resource not found' });
});
7. Implement Secure Session Management
Use express-session with secure settings:
npm install express-session
Implementation:
const express = require('express');
const session = require('express-session');
const app = express();
app.use(
session({
secret: process.env.SESSION_SECRET,
name: 'sessionId', // Instead of default 'connect.sid'
cookie: {
httpOnly: true, // Prevents client-side JavaScript from reading the cookie
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
sameSite: 'strict', // Helps prevent CSRF
maxAge: 1000 * 60 * 60 * 24 // 24 hours
},
resave: false,
saveUninitialized: false
})
);
app.get('/login', (req, res) => {
req.session.userId = 'user123';
res.send('Logged in');
});
app.get('/profile', (req, res) => {
if (req.session.userId) {
res.send(`Welcome user ${req.session.userId}`);
} else {
res.redirect('/login');
}
});
For production applications, use a more secure session store like Redis:
npm install connect-redis redis
const redis = require('redis');
const connectRedis = require('connect-redis');
const session = require('express-session');
const RedisStore = connectRedis(session);
const redisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
// other options as above
})
);
8. Configure CORS Properly
Use cors to control access to your API:
npm install cors
Implementation:
const express = require('express');
const cors = require('cors');
const app = express();
// Basic CORS setup - allows any origin (not recommended for production)
// app.use(cors());
// More secure CORS configuration
app.use(
cors({
origin: 'https://yourtrustedwebsite.com', // Specify allowed origins
methods: ['GET', 'POST'], // Allowed methods
allowedHeaders: ['Content-Type', 'Authorization'], // Allowed headers
credentials: true // Allows cookies to be sent with requests
})
);
// You can also enable CORS for specific routes
app.get('/api/public-data', cors(), (req, res) => {
res.json({ message: 'This is public data' });
});
9. Secure File Uploads
If your application accepts file uploads, ensure they're secure:
npm install multer
Implementation:
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, './uploads/');
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
// File filter
const fileFilter = (req, file, cb) => {
// Accept only specific file types
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
cb(null, true);
} else {
cb(new Error('Unsupported file format'), false);
}
};
// Set up multer
const upload = multer({
storage: storage,
limits: {
fileSize: 1024 * 1024 * 5 // 5MB
},
fileFilter: fileFilter
});
// Handle single file upload
app.post('/upload', upload.single('image'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'Please provide an image' });
}
res.json({
message: 'File uploaded successfully',
fileName: req.file.filename
});
});
10. Use HTTPS in Production
Always use HTTPS in production. In Express:
const express = require('express');
const https = require('https');
const fs = require('fs');
const app = express();
// Your routes and middleware here
// For development with self-signed certificates
if (process.env.NODE_ENV !== 'production') {
const options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert')
};
https.createServer(options, app).listen(443, () => {
console.log('HTTPS server running on port 443');
});
} else {
// For production, you might use a service like Heroku which handles HTTPS
app.listen(process.env.PORT || 3000, () => {
console.log('Server running on port', process.env.PORT || 3000);
});
}
In production, you should:
- Use a service like Let's Encrypt for free SSL certificates
- Configure your reverse proxy (Nginx, Apache) or use a PaaS provider that handles SSL termination
Real-World Example: Building a Secure API
Let's create a complete example of a secure Express API endpoint:
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const { body, validationResult } = require('express-validator');
const jwt = require('jsonwebtoken');
require('dotenv').config();
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.CORS_ORIGIN,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(express.json({ limit: '10kb' })); // Limit request size
// Rate limiting
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/', apiLimiter);
// Authentication middleware
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
};
// Login endpoint with validation and rate limiting
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 5,
message: 'Too many login attempts, please try again later'
});
app.post('/api/login',
loginLimiter,
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 })
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// In a real app, verify credentials against database
// This is just an example
if (req.body.email === '[email protected]' && req.body.password === 'securepassword') {
const user = { id: 1, email: req.body.email };
const token = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
}
);
// Protected route
app.get('/api/profile', authenticateToken, (req, res) => {
// In a real app, fetch user data from database
res.json({
id: req.user.id,
email: req.user.email,
name: 'John Doe',
// Don't include sensitive info like password, even hashed
});
});
// Error handling
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Something went wrong'
: err.message
});
});
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Resource not found' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Security Testing Tools
To verify your Express application's security, consider using these tools:
- OWASP ZAP - Open-source security scanner
- Snyk - Dependency vulnerability scanner
- npm audit - Built-in dependency scanner
- helmet-csp tester - Test your Content Security Policy
- SSL Labs - Test your HTTPS configuration
Summary
Implementing proper security measures in your Express application is essential for protecting your users and data. This checklist covers the fundamental security practices:
✅ Use Helmet for security headers
✅ Prevent CSRF attacks
✅ Validate and sanitize all user input
✅ Implement rate limiting
✅ Use environment variables for sensitive data
✅ Set up proper error handling
✅ Implement secure session management
✅ Configure CORS properly
✅ Secure file uploads
✅ Use HTTPS in production
Remember that security is an ongoing process. Stay updated on new threats and regularly audit your application's security.
Additional Resources
- Express.js Security Best Practices
- OWASP Top 10
- Node.js Security Checklist
- Helmet.js Documentation
- Express Validator Documentation
Practice Exercises
-
Security Audit: Run
npm audit
on your existing Express application and fix any vulnerabilities found. -
Helmet Configuration: Implement Helmet in your application with a custom Content Security Policy that allows scripts only from your domain.
-
Rate Limiter: Add rate limiting to your authentication routes to prevent brute force attacks.
-
Secure API: Create a simple API with proper validation, authentication, and CORS configuration.
-
Security Headers Check: Use a tool like securityheaders.com to analyze your application's HTTP headers and improve any deficiencies.
By combining these practices, you'll significantly improve the security posture of your Express applications and build a strong foundation of security knowledge for your web development career.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)