Dictionary Attack in Express with Basic Auth
Dictionary Attack in Express with Basic Auth — how this specific combination creates or exposes the vulnerability
A dictionary attack in an Express API that uses HTTP Basic Authentication attempts many username and password combinations against the same endpoint to find valid credentials. Because Basic Auth encodes credentials in an RFC 7617 header (Authorization: Basic base64(username:password)) rather than sending them in the request body, automated tools can iterate rapidly with minimal friction. If the endpoint lacks effective rate limiting, each attempt returns a distinct HTTP status (e.g., 401 vs 200), which allows an attacker to validate credentials without triggering account lockouts. In Express, routes like POST /login that perform a direct lookup and respond with 401 for unknown users make it straightforward for a scanner to enumerate valid users via timing and status differences.
When combined with unauthenticated scanning, middleBrick runs authentication checks in parallel and can detect whether your Express routes leak user enumeration via status codes or timing. These checks are part of the Authentication and BOLA/IDOR security categories. An API that returns 200 for a correct password and 401 for an incorrect one makes it trivial to automate dictionary-style brute force. Even with Base64 encoding, credentials are not encrypted; without TLS, they are sent in clear text across the network. The scanner also tests for missing rate limiting, which would otherwise slow down or block high-volume attempts, and flags endpoints where weak password policies make dictionary lists effective.
In a black-box scan, middleBrick submits a list of common usernames and passwords to gauge whether the Express app is vulnerable to credential guessing. The tool checks whether responses are consistent in timing and content to avoid leaking which part of the credential pair was wrong. Findings include whether the endpoint is susceptible to credential enumeration, whether transport encryption is enforced, and whether controls like rate limiting or MFA would reduce risk. These results appear in the per-category breakdown and prioritized findings with severity and remediation guidance, helping you understand how an attacker might chain a dictionary attack with other techniques.
Basic Auth-Specific Remediation in Express — concrete code fixes
To harden Express APIs using HTTP Basic Authentication, enforce TLS for all traffic, avoid user enumeration through consistent responses, implement rate limiting, and move toward token-based or session-based authentication where feasible. The following patterns demonstrate secure handling when Basic Auth is still required.
- Always use HTTPS to protect credentials in transit. Without TLS, base64-encoded credentials are easily decoded.
- Return the same HTTP status and generic message for invalid credentials to prevent user enumeration.
- Apply rate limiting to reduce the effectiveness of high-volume dictionary attempts.
- Prefer stronger mechanisms (e.g., OAuth2, session cookies with secure flags, or API keys) over Basic Auth for production APIs.
Example: Secure Express route with Basic Auth, HTTPS enforcement, and rate limiting
const express = require('express');
const rateLimit = require('express-rate-limit');
const https = require('https');
const fs = require('fs');
const app = express();
// Enforce HTTPS in production by rejecting non-secure requests
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'production' && !req.secure) {
return res.status(403).json({ error: 'HTTPS required' });
}
next();
});
// Rate limiting to mitigate dictionary attacks
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 30, // limit each IP to 30 requests per window
message: { error: 'Too many attempts, try again later' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/login', authLimiter);
// Basic Auth validation without user enumeration
const users = {
alice: 'correctHorseBatteryStaple', // In practice, store password hashes
};
app.post('/login', (req, res) => {
const authHeader = req.headers.authorization || '';
const match = authHeader.match(/^Basic\s+(\S+)$/i);
if (!match) {
return res.status(401).json({ error: 'Authentication required' });
}
const decoded = Buffer.from(match[1], 'base64').toString('utf-8');
const separatorIndex = decoded.indexOf(':');
if (separatorIndex === -1) {
return res.status(401).json({ error: 'Invalid credentials format' });
}
const username = decoded.slice(0, separatorIndex);
const password = decoded.slice(separatorIndex + 1);
// Constant-time comparison to reduce timing leakage
const expected = users[username];
const isValid = expected && timingSafeEqual(password, expected);
if (!isValid) {
// Always return the same status and generic message
return res.status(401).json({ error: 'Invalid credentials' });
}
// Issue a token or session in a real implementation
res.json({ message: 'Authenticated', user: username });
});
// Simple constant-time string comparison to mitigate timing attacks
function timingSafeEqual(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') return false;
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;
}
https.createServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
}, app).listen(443);
In this example, the login route is protected by rate limiting, enforces HTTPS in production, avoids revealing whether a username exists, and uses a constant-time comparison to reduce timing side channels. For stronger security, replace static credentials with a salted hash store (e.g., bcrypt) and migrate to token-based flows where possible. middleBrick’s scans can validate whether these protections are observable in runtime behavior and will flag endpoints that remain vulnerable to dictionary attacks.