Password Spraying in Express
How Password Spraying Manifests in Express
Password spraying in Express applications typically exploits authentication endpoints that accept username/password combinations without adequate rate limiting or credential validation. The attack works by attempting a small set of common passwords across many valid usernames, avoiding detection mechanisms that trigger on single-account brute force attempts.
In Express, password spraying commonly targets authentication routes like:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ where: { username } });
if (!user) {
// Critical vulnerability: returning early without rate limiting
return res.status(401).json({ error: 'Invalid credentials' });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
// No rate limiting here either
return res.status(401).json({ error: 'Invalid credentials' });
}
// Successful authentication
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
res.json({ token });
});The vulnerability lies in the lack of rate limiting and the early return pattern. When a username doesn't exist, the endpoint returns immediately without any throttling. Attackers can send thousands of requests with non-existent usernames and common passwords, mapping out valid accounts through timing differences or error responses.
Express middleware like express-rate-limit is often missing or misconfigured. A typical vulnerable setup:
// ❌ INSECURE: No rate limiting on authentication
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ where: { email } });
if (!user || !(await bcrypt.compare(password, user.password))) {
// No delay, no counter, no blocking
return res.status(401).json({ error: 'Authentication failed' });
}
res.json({ token: generateToken(user) });
});Attackers can automate this using tools like Burp Suite or custom scripts, sending requests like:
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json
{
"username": "admin",
"password": "Password123!"
}The lack of consistent response timing between valid and invalid usernames makes detection easier for attackers. Some Express applications inadvertently leak information through response times or error messages, further facilitating password spraying campaigns.
Express-Specific Detection
Detecting password spraying in Express requires monitoring authentication endpoints for specific patterns. middleBrick's black-box scanning approach tests these vulnerabilities without requiring access to source code or credentials.
middleBrick scans Express authentication endpoints by:
- Identifying login routes through OpenAPI spec analysis or runtime discovery
- Testing with common password lists across multiple usernames
- Analyzing response patterns, timing, and error codes
- Checking for missing rate limiting middleware
- Verifying proper error handling consistency
The scanner specifically looks for Express middleware configurations that might be missing or improperly set up. For example, it checks if express-rate-limit is configured with appropriate windows and limits:
// middleBrick detection patterns
// ❌ MISSING: No rate limiting on /login
app.post('/login', authHandler);middleBrick's LLM security checks also detect if authentication endpoints are exposed to AI services or if system prompts contain credential-related information that could be leaked through prompt injection attacks.
Real-world detection scenarios include:
| Detection Pattern | Risk Level | middleBrick Finding |
|---|---|---|
| No rate limiting on /auth/login | High | "Authentication endpoint lacks rate limiting - vulnerable to password spraying" |
| Early returns without throttling | High | "Authentication endpoint returns immediately for invalid credentials" | Inconsistent response timing | Medium | "Response timing varies between valid/invalid usernames" |
middleBrick's continuous monitoring (Pro plan) can track authentication endpoint security over time, alerting when new vulnerabilities are introduced or when attack patterns are detected.
Express-Specific Remediation
Remediating password spraying in Express requires implementing proper rate limiting, consistent error handling, and secure authentication patterns. Here's how to fix these vulnerabilities using Express's native capabilities:
1. Implement rate limiting on all authentication endpoints:
const rateLimit = require('express-rate-limit');
// Limit to 5 attempts per 15 minutes per IP
const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests
message: {
error: 'Too many authentication attempts. Try again later.'
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.ip
});
// Apply to all auth routes
app.use('/auth/login', loginRateLimiter);
app.use('/auth/forgot-password', loginRateLimiter);
app.use('/auth/reset-password', loginRateLimiter);
2. Use consistent response patterns to prevent timing attacks:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Always perform a database lookup, even for non-existent users
const user = await User.findOne({ where: { username } }) || { password: '' };
// Simulate hash comparison time for non-existent users
const exists = user.id !== undefined;
const valid = await bcrypt.compare(password, user.password);
// Always perform the same operations regardless of outcome
await logAuthenticationAttempt(username, valid && exists);
if (!valid || !exists) {
// Add delay to prevent timing attacks
await delay(300);
return res.status(401).json({
error: 'Invalid credentials'
});
}
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
res.json({ token });
});
// Utility function to add constant delay
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
3. Implement account lockout mechanisms:
const MAX_FAILED_ATTEMPTS = 5;
const LOCKOUT_DURATION = 30 * 60 * 1000; // 30 minutes
app.post('/login', async (req, res) => {
const { username } = req.body;
const user = await User.findOne({ where: { username } });
if (user && user.failedAttempts >= MAX_FAILED_ATTEMPTS) {
const now = Date.now();
const lockoutEnd = user.lockoutUntil || 0;
if (now < lockoutEnd) {
const remaining = Math.ceil((lockoutEnd - now) / 1000);
return res.status(429).json({
error: 'Account temporarily locked',
retryAfter: remaining
});
}
// Reset failed attempts after lockout period
user.failedAttempts = 0;
await user.save();
}
// Authentication logic...
});
4. Add CAPTCHA or 2FA after multiple failed attempts:
const captchaMiddleware = require('./captcha-middleware');
app.post('/login', loginRateLimiter, async (req, res, next) => {
const { username } = req.body;
const user = await User.findOne({ where: { username } });
if (user && user.failedAttempts >= 3) {
return captchaMiddleware(req, res, next);
}
next();
}, authHandler);middleBrick's GitHub Action integration can automatically scan your Express application's authentication endpoints during CI/CD, failing builds if password spraying vulnerabilities are detected.