Denial Of Service in Express with Basic Auth
Denial Of Service in Express with Basic Auth — how this specific combination creates or exposes the vulnerability
When Basic Authentication is applied in Express, the interaction between credential parsing and request processing can amplify Denial of Service (DoS) risks. Basic Auth transmits credentials in an encoded (not encrypted) header on each request. If the server performs synchronous, CPU-intensive work—such as bcrypt comparison—before early checks like rate limiting or input validation, an attacker can exhaust event loop capacity or Node.js worker threads using many concurrent requests with invalid credentials.
In an unauthenticated scan, middleBrick tests endpoints that accept Basic Auth headers. It looks for indicators such as absence of per-route rate limiting, long-running synchronous operations on auth failures, and missing concurrency guards. For example, an endpoint that calls bcrypt.compareSync on every request with a malformed header can block the Node.js event loop. This becomes a DoS vector because the server stops accepting new connections until the blocking work finishes. The scan also flags missing connection limits and missing timeouts on authentication middleware, which can allow slowloris-style header attacks to keep sockets open.
Additionally, if the server leaks information about whether a username exists before performing the costliest operation, an attacker can combine this with credential spraying to amplify resource usage per username. middleBrick checks whether authentication failures follow a consistent timing pattern and whether error responses omit stack traces while still performing expensive work. Findings include lack of concurrency throttling, missing request timeouts on auth middleware, and absence of short-circuit validation (e.g., rejecting malformed headers before password hashing).
Basic Auth-Specific Remediation in Express — concrete code fixes
Apply authentication and rate limiting early, use constant-time comparison where possible, and avoid blocking operations in the event loop. Below are concrete Express patterns that reduce DoS risk when Basic Auth is required.
- Validate headers and reject malformed requests before expensive work:
import express from 'express';
import { authHeaderParser } from './auth-utils';
const app = express();
app.use((req, res, next) => {
const header = req.headers.authorization;
if (!header || !header.startsWith('Basic ')) {
return res.status(400).send('Authorization header required');
}
const token = header.split(' ')[1];
if (!token || !Buffer.from(token, 'base64').toString('utf-8')) {
return res.status(400).send('Invalid authorization header format');
}
next();
});
- Apply per-route rate limiting to protect authentication paths:
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 30,
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests from this IP, try again later.',
});
// Apply to auth endpoints and sensitive routes
app.post('/login', authLimiter, (req, res) => {
const { username, password } = req.body;
// handle login
});
- Use asynchronous hashing with timeouts and avoid synchronous blocking:
import bcrypt from 'bcrypt';
import { setTimeout as sleep } from 'timers/promises';
// Use async compare with an overall request timeout
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const candidateHash = await getUserHash(username); // your user lookup
if (!candidateHash) {
// Still perform a dummy hash to keep timing consistent
await bcrypt.hash(password, 10);
return res.status(401).send('Unauthorized');
}
try {
const hashWork = bcrypt.compare(password, candidateHash);
const timeout = sleep(2000); // safety timeout in ms
const [result] = await Promise.race([hashWork, timeout]);
if (result) {
res.status(200).send('OK');
} else {
res.status(401).send('Unauthorized');
}
} catch (err) {
res.status(500).send('Internal error');
}
});
- Set server and proxy timeouts and limit payload sizes to reduce resource exhaustion:
import express from 'express';
const app = express();
// limit payload size to mitigate memory exhaustion
app.use(express.json({ limit: '10kb' }));
app.use(express.text({ type: '*/*', limit: '10kb' }));
// Ensure keep-alive timeouts are managed at the infrastructure level
// (configure in reverse proxy or load balancer)
These patterns ensure authentication checks are fast-fail, resource usage is bounded, and expensive operations are asynchronous and guarded by timeouts, reducing the likelihood of DoS when Basic Auth is used.
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 |