HIGH denial of serviceexpress

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 IDNameSeverity
CWE-400Uncontrolled Resource Consumption HIGH
CWE-770Allocation of Resources Without Limits MEDIUM
CWE-799Improper Control of Interaction Frequency MEDIUM
CWE-835Infinite Loop HIGH
CWE-1050Excessive 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.