Dictionary Attack in Express with Api Keys
Dictionary Attack in Express with Api Keys — how this specific combination creates or exposes the vulnerability
A dictionary attack in an Express API that uses API keys typically involves an attacker attempting many candidate keys in rapid succession to find a valid key. Because API keys are often passed in headers (e.g., x-api-key) and sometimes stored or compared in a non‑constant‑time way, an endpoint that does not enforce rate limiting or lockout can become vulnerable. If the server leaks whether a key is valid through timing differences or distinct HTTP responses, attackers can iteratively guess keys from a dictionary and progressively identify valid credentials.
Express APIs commonly expose this risk when authentication is implemented as a simple lookup without additional protections. For example, an endpoint that reads req.headers['x-api-key'] and immediately returns 200 for a match and 401 for no match can allow timing-based inference. Attackers may use tools to send thousands of requests per minute, observing response times or status codes to refine guesses. Even if keys are stored as hashes, insufficient rate limiting enables online guessing at scale.
Because API keys are often long‑lived and shared across services, a successful dictionary attack can lead to broader access across microservices. In an Express app, this risk is compounded when keys are passed in URLs or logs, or when the same key is used across multiple environments. MiddleBrick’s 12 security checks run in parallel and include Authentication and Rate Limiting assessments, which can identify whether your Express endpoints leak validity signals and allow excessive attempts without detection.
To illustrate, consider an Express route that compares keys naively:
const apiKeys = new Set(['abc123', 'def456']);
app.get('/data', (req, res) => {
const key = req.headers['x-api-key'];
if (!key) return res.status(401).send('Missing key');
if (apiKeys.has(key)) return res.json({ ok: true });
res.status(401).send('Invalid key');
});
This code reveals key validity via status code and does not limit request rate, making dictionary attacks feasible. Attackers can send a list of candidate keys and observe which ones return 200, effectively performing an online dictionary attack.
MiddleBrick scans such endpoints unauthenticated and flags findings tied to Authentication and Rate Limiting, providing prioritized findings with severity and remediation guidance. Without protections like constant‑time comparison, strict rate limits, and monitoring for bursts of failed attempts, an Express API using API keys remains exposed to dictionary attacks.
Api Keys-Specific Remediation in Express — concrete code fixes
Remediation focuses on making dictionary attacks impractical by removing timing leaks, enforcing rate limits, and handling keys safely. Below are concrete fixes and code examples for an Express API.
1. Constant‑time comparison
Use a constant‑time comparison to prevent attackers from inferring partial matches based on response time. Replace direct equality checks with a utility that always runs in the same number of operations.
function safeCompare(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
const validKey = crypto.timingSafeEqual
? (buf1, buf2) => crypto.timingSafeEqual(Buffer.from(buf1), Buffer.from(buf2))
: safeCompare;
2. Rate limiting and attempt throttling
Apply rate limiting per key or per IP to slow down guessing. Use a sliding window or token‑bucket approach and ensure failures and successes are both counted toward limits.
const rateLimit = require('express-rate-limit');
const keyLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per window
message: { error: 'Too many requests, try again later.' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/data', keyLimiter);
3. Avoid sending distinct status codes for missing vs invalid keys
Return the same generic response for missing and invalid keys to remove informational signals.
app.get('/data', (req, res) => {
const candidate = req.headers['x-api-key'] || '';
const isValid = validKey(candidate, Buffer.from(process.env.STORED_API_KEY_HASH));
// Constant-time response shape regardless of validity
res.status(200).json({ ok: isValid });
});
4. Store and compare hashes, not plaintext keys
Never store API keys in plaintext. Store a salted hash and compare using a constant‑time routine. If you must accept plaintext keys at the edge, hash them immediately and compare hashes.
const crypto = require('crypto');
const storedHash = crypto.createHmac('sha256', process.env.SALT)
.update(process.env.RAW_API_KEY)
.digest('hex');
// During verification:
const inputHash = crypto.createHmac('sha256', process.env.SALT)
.update(candidate)
.digest('hex');
const isValid = safeCompare(inputHash, storedHash);
5. Use middleware to centralize checks and logging
Centralize validation to reduce mistakes and ensure consistent behavior across routes.
app.use((req, res, next) => {
const key = req.headers['x-api-key'] || '';
const expected = process.env.STORED_API_KEY_HASH;
const valid = validKey(key, expected);
if (!valid) {
// Log failed attempts for monitoring, but return generic response
console.warn('Invalid API key attempt');
}
// Proceed even if invalid; route handlers will re-check if needed
next();
});
Implementing these measures significantly reduces the feasibility of dictionary attacks against an Express API using API keys. MiddleBrick’s scans can verify that such protections are in place and highlight remaining gaps in Authentication and Rate Limiting.