Skip to main content

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:

bash
npm install helmet

Implementation:

javascript
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 attacks
  • X-XSS-Protection to enable browser XSS filters
  • X-Frame-Options to prevent clickjacking
  • X-Content-Type-Options to prevent MIME-sniffing
  • And several others

You can also configure specific headers:

javascript
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:

bash
npm install csurf

Implementation:

javascript
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):

html
<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:

bash
npm install express-validator

Implementation:

javascript
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:

bash
npm install express-rate-limit

Implementation:

javascript
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:

bash
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:

javascript
// 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:

javascript
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:

bash
npm install express-session

Implementation:

javascript
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:

bash
npm install connect-redis redis
javascript
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:

bash
npm install cors

Implementation:

javascript
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:

bash
npm install multer

Implementation:

javascript
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:

javascript
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:

javascript
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:

  1. OWASP ZAP - Open-source security scanner
  2. Snyk - Dependency vulnerability scanner
  3. npm audit - Built-in dependency scanner
  4. helmet-csp tester - Test your Content Security Policy
  5. 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

Practice Exercises

  1. Security Audit: Run npm audit on your existing Express application and fix any vulnerabilities found.

  2. Helmet Configuration: Implement Helmet in your application with a custom Content Security Policy that allows scripts only from your domain.

  3. Rate Limiter: Add rate limiting to your authentication routes to prevent brute force attacks.

  4. Secure API: Create a simple API with proper validation, authentication, and CORS configuration.

  5. 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! :)