Denial Of Service in Express
How Denial Of Service Manifests in Express
Denial of Service (DoS) attacks targeting Express applications exploit the framework's synchronous nature and Node.js's single-threaded event loop. When Express receives requests that block the event loop, the entire application becomes unresponsive to legitimate users.
The most common Express-specific DoS vectors involve blocking operations in middleware and route handlers. Consider this problematic pattern:
app.get('/slow', (req, res) => {
// Synchronous CPU-intensive operation
let result = 0;
for (let i = 0; i < 1e9; i++) {
result += Math.random();
}
res.json({ result });
});
This code blocks the event loop for seconds, preventing Express from processing any other requests during that time. An attacker can exploit this by repeatedly hitting the endpoint, effectively shutting down your API.
Another Express-specific vulnerability involves synchronous file operations:
app.get('/report', (req, res) => {
const data = fs.readFileSync('./large-file.txt'); // Blocks event loop
res.send(data);
});
Express's middleware chain amplifies DoS risks. A single malicious request can trigger expensive operations across multiple middleware layers:
function expensiveAuthMiddleware(req, res, next) {
// Synchronous database query blocks event loop
const user = db.querySync('SELECT * FROM users WHERE token = ?', [req.headers.token]);
req.user = user;
next();
}
app.get('/api/data', expensiveAuthMiddleware, (req, res) => {
res.json({ data: processExpensiveData(req.user) });
});
Express applications are particularly vulnerable to request body attacks. Without proper limits, attackers can send massive payloads that consume memory:
// Vulnerable: no body size limits
app.use(express.json());
app.post('/upload', (req, res) => {
// 100MB JSON payload crashes the process
const data = req.body;
res.json({ size: Buffer.byteLength(JSON.stringify(data), 'utf8') });
});
Regular expression denial of service (ReDoS) is especially dangerous in Express route handlers where user input is validated:
app.post('/search', (req, res) => {
const unsafeRegex = /(a+)+$/;
const query = req.body.query;
// Vulnerable to catastrophic backtracking
if (unsafeRegex.test(query)) {
// This can take seconds or minutes
res.json({ matches: findAllMatches(query) });
}
});
Express's default error handling can also be exploited. Uncaught exceptions in synchronous code crash the entire process:
app.get('/crash', (req, res) => {
// Throws error, crashes process
JSON.parse('invalid json');
});
Express-Specific Detection
Detecting DoS vulnerabilities in Express requires both runtime monitoring and static analysis. middleBrick's Express-specific scanning identifies these patterns automatically:
Runtime Monitoring involves tracking event loop lag and request processing times. Express applications should monitor:
const express = require('express');
const app = express();
// Monitor event loop lag
let lastCheck = process.hrtime();
setInterval(() => {
const diff = process.hrtime(lastCheck);
const lag = diff[0] * 1000 + diff[1] / 1e6 - 1000; // ms behind
if (lag > 100) {
console.warn(`Event loop lag detected: ${lag.toFixed(2)}ms`);
}
lastCheck = process.hrtime();
}, 1000);
Static Analysis with middleBrick scans your Express codebase for blocking patterns:
$ middlebrick scan http://localhost:3000/api
=== API Security Report ===
Score: B (82/100)
Findings:
- Synchronous operations in route handlers (Severity: High)
- Missing body size limits (Severity: Medium)
- Unbounded request processing (Severity: Medium)
Recommendations:
- Replace blocking operations with async/await
- Add express.json({ limit: '10mb' }) middleware
- Implement rate limiting with express-rate-limit
Middleware Inspection reveals hidden DoS vectors. middleBrick analyzes your middleware chain for:
| Vulnerability Type | Detection Pattern | Express Impact |
|---|---|---|
| Synchronous Database Calls | db.querySync(), findSync() | Blocks entire request queue |
| Blocking File Operations | readFileSync(), writeFileSync() | Halts event loop |
| Unbounded Body Parsing | express.json() without limits | Memory exhaustion |
| Recursive Operations | Deep recursion in handlers | Stack overflow |
Performance Profiling helps identify DoS-prone endpoints. Use Node.js's built-in profiler:
const inspector = require('inspector');
const session = new inspector.Session();
session.connect();
session.post('Profiler.enable', () => {
session.post('Profiler.start', () => {
// Let traffic flow for 30 seconds
setTimeout(() => {
session.post('Profiler.stop', (err, { profile }) => {
console.log(JSON.stringify(profile, null, 2));
});
}, 30000);
});
});
Rate Limiting Analysis checks if your Express app has proper request throttling:
const rateLimit = require('express-rate-limit');
// Check if rate limiting is properly configured
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests
message: 'Too many requests from this IP'
});
app.use(limiter);
Express-Specific Remediation
Remediating DoS vulnerabilities in Express requires architectural changes and defensive coding patterns. Here's how to secure your Express application:
1. Eliminate Blocking Operations
// Vulnerable: Synchronous database call
app.get('/users', (req, res) => {
const users = db.querySync('SELECT * FROM users'); // BLOCKS
res.json(users);
});
// Secure: Asynchronous with proper error handling
app.get('/users', async (req, res, next) => {
try {
const users = await db.queryAsync('SELECT * FROM users');
res.json(users);
} catch (error) {
next(error);
}
});
2. Implement Request Timeouts
const timeout = require('connect-timeout');
// Global timeout for all requests
app.use(timeout('30s'));
// Handle timeout errors
app.use((req, res, next) => {
if (!req.timedout) return next();
res.status(503).json({ error: 'Request timeout' });
});
// Per-route timeout for expensive operations
app.get('/heavy', timeout('10s'), async (req, res, next) => {
try {
const result = await processHeavyOperation();
res.json(result);
} catch (error) {
next(error);
}
});
3. Add Body Size Limits
// Configure body parser with limits
app.use(express.json({
limit: '10mb',
strict: true,
type: 'application/json'
}));
app.use(express.urlencoded({
limit: '10mb',
extended: true
}));
// Custom middleware for additional validation
app.use((req, res, next) => {
if (req.headers['content-length'] > 10 * 1024 * 1024) {
return res.status(413).json({ error: 'Payload too large' });
}
next();
});
4. Implement Rate Limiting
const rateLimit = require('express-rate-limit');
// Rate limit by IP address
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false
});
// Rate limit by API key for authenticated users
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 1000,
keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
message: 'Too many API requests'
});
// Apply rate limiting globally
app.use(generalLimiter);
// Apply API-specific rate limiting
app.use('/api/', apiLimiter);
// Disable rate limiting for health checks
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
5. Use Clustering for Load Distribution
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
// Fork workers based on CPU cores
for (let i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // Restart failed worker
});
} else {
const express = require('express');
const app = express();
// Worker processes share same code
app.get('/', (req, res) => {
res.send(`Worker ${process.pid} says hello`);
});
app.listen(3000, () => {
console.log(`Worker ${process.pid} started`);
});
}
6. Implement Circuit Breaker Pattern
const CircuitBreaker = require('opossum');
// Create circuit breaker for external API calls
const breaker = new CircuitBreaker(expensiveOperation, {
timeout: 10000,
maxFailures: 5,
resetTimeout: 30000
});
breaker.on('open', () => {
console.log('Circuit breaker opened');
});
breaker.on('halfOpen', () => {
console.log('Circuit breaker half open');
});
app.get('/external', async (req, res) => {
try {
const result = await breaker.fire();
res.json(result);
} catch (error) {
res.status(503).json({ error: 'Service temporarily unavailable' });
}
});
async function expensiveOperation() {
// Simulate external API call
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ data: 'external service response' });
}, 5000);
});
}
Related CWEs: resourceConsumption
| CWE ID | Name | Severity |
|---|---|---|
| CWE-400 | Uncontrolled Resource Consumption | HIGH |
| CWE-770 | Allocation of Resources Without Limits | MEDIUM |
| CWE-799 | Improper Control of Interaction Frequency | MEDIUM |
| CWE-835 | Infinite Loop | HIGH |
| CWE-1050 | Excessive Platform Resource Consumption | MEDIUM |
Frequently Asked Questions
How can I test my Express application for DoS vulnerabilities?
Use middleBrick's CLI tool to scan your Express API endpoints: middlebrick scan http://localhost:3000. The scanner tests for blocking operations, missing rate limits, and unbounded request processing. For manual testing, use tools like autocannon or wrk to simulate high traffic and monitor your application's response times and memory usage.
What's the difference between DoS and DDoS in Express applications?
DoS attacks come from a single source and target specific Express endpoints with blocking operations or resource exhaustion. DDoS attacks involve multiple sources and are harder to mitigate because they overwhelm your entire infrastructure. Express applications are particularly vulnerable to DoS because Node.js's single-threaded nature means one blocking request can stall the entire event loop, affecting all users regardless of the attack's scale.