Brute Force Attack in Express
How Brute Force Attack Manifests in Express
Brute force attacks in Express applications typically target authentication endpoints where attackers systematically try username/password combinations to gain unauthorized access. In Express, this often manifests at login routes like /login, /auth, or any custom authentication endpoint.
A common Express-specific pattern that enables brute force attacks is the use of asynchronous authentication middleware without rate limiting. Consider this vulnerable pattern:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (user && await bcrypt.compare(password, user.password)) {
req.session.user = user;
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});This endpoint is vulnerable because it provides no protection against rapid repeated attempts. An attacker can send thousands of requests per minute, each triggering a database query and bcrypt comparison. The bcrypt comparison itself becomes a performance bottleneck, as each failed attempt still requires computationally expensive password hashing.
Another Express-specific manifestation occurs with JWT token generation. Many developers implement token refresh endpoints without proper throttling:
app.post('/refresh', async (req, res) => {
const token = req.cookies.refreshToken;
if (token) {
const payload = jwt.verify(token, process.env.REFRESH_SECRET);
const user = await User.findById(payload.userId);
if (user) {
const newToken = generateJWT(user);
res.json({ token: newToken });
}
}
res.status(401).json({ error: 'Invalid token' });
});Without rate limiting, this endpoint can be abused to cause denial of service through excessive JWT generation, consuming CPU resources and potentially database connections.
Express middleware order also creates specific vulnerabilities. If authentication middleware is applied before rate limiting middleware, the authentication logic executes before the rate check, wasting resources on every request:
// VULNERABLE ORDER
app.use(authenticateJWT);
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));The correct order should be:
// SECURE ORDER
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use(authenticateJWT);Additionally, Express applications often expose admin interfaces or debug endpoints that lack authentication entirely, creating perfect targets for brute force attacks:
// DANGEROUS: admin endpoint without auth
app.get('/admin/stats', (req, res) => {
res.json(getSystemStats());
});Even if this endpoint doesn't contain sensitive data, it can be used as a measurement tool for timing attacks during brute force attempts against other endpoints.
Express-Specific Detection
Detecting brute force vulnerabilities in Express requires examining both the application code and runtime behavior. For code analysis, look for authentication endpoints that lack rate limiting middleware. middleBrick's Express-specific scanning identifies these patterns by analyzing the route structure and middleware chain.
When scanning an Express application with middleBrick, the tool examines your OpenAPI/Swagger spec (if available) and maps it to the actual runtime endpoints. For brute force detection, middleBrick specifically looks for:
- POST endpoints with authentication-related paths (/login, /auth, /signin, etc.)
- Endpoints that accept credentials without rate limiting
- Password reset or recovery endpoints
- Token refresh endpoints
- Admin or debug endpoints without authentication
The scanner tests these endpoints by sending repeated requests and measuring response times and error patterns. A vulnerable endpoint will show consistent response times even under load, while a protected endpoint will demonstrate rate limiting behavior through HTTP 429 responses or increasing response delays.
For runtime detection in production Express apps, implement request logging middleware to track authentication failures:
const failedLogins = new Map();
app.use((req, res, next) => {
if (req.path === '/login' && req.method === 'POST') {
const ip = req.ip || req.connection.remoteAddress;
const failures = failedLogins.get(ip) || 0;
if (failures > 10) {
console.warn(`Suspicious activity from ${ip}: ${failures} failed attempts`);
}
}
next();
});middleBrick's API security scanning complements this by providing a comprehensive assessment without requiring code changes. The scanner tests the actual attack surface that's exposed to the internet, catching configuration issues that code review might miss.
Another detection method involves analyzing error responses. Vulnerable Express endpoints often return detailed error messages that help attackers:
// VULNERABLE: detailed error messages
if (!user) {
return res.status(401).json({
error: 'User not found',
code: 'USER_NOT_FOUND'
});
}
if (!validPassword) {
return res.status(401).json({
error: 'Invalid password',
code: 'INVALID_PASSWORD'
});
}middleBrick flags these patterns because they allow attackers to enumerate valid usernames before attempting password brute force.
Express-Specific Remediation
Express provides several native approaches for mitigating brute force attacks. The most effective is using the express-rate-limit middleware specifically on authentication endpoints:
const rateLimit = require('express-rate-limit');
const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
message: {
error: 'Too many login attempts, please try again later.'
},
standardHeaders: true,
legacyHeaders: false,
});
app.post('/login', loginRateLimiter, async (req, res) => {
// authentication logic
});For more sophisticated protection, implement IP-based and account-based rate limiting:
const loginAttempts = new Map();
const checkBruteForce = async (req, res, next) => {
const ip = req.ip;
const { username } = req.body;
const ipAttempts = loginAttempts.get(ip) || { count: 0, last: Date.now() };
const accountAttempts = loginAttempts.get(username) || { count: 0, last: Date.now() };
const now = Date.now();
const fifteenMinutes = 15 * 60 * 1000;
// Clean old attempts
if (now - ipAttempts.last > fifteenMinutes) {
loginAttempts.delete(ip);
}
if (now - accountAttempts.last > fifteenMinutes) {
loginAttempts.delete(username);
}
if (ipAttempts.count > 10 || accountAttempts.count > 10) {
return res.status(429).json({
error: 'Too many attempts, try again later'
});
}
next();
};
app.post('/login', checkBruteForce, async (req, res) => {
const { username } = req.body;
const ip = req.ip;
// Increment counters
loginAttempts.set(ip, {
count: (loginAttempts.get(ip)?.count || 0) + 1,
last: Date.now()
});
loginAttempts.set(username, {
count: (loginAttempts.get(username)?.count || 0) + 1,
last: Date.now()
});
// authentication logic
});Another Express-specific mitigation is using express-slow-down for progressive delays:
const slowDown = require('express-slow-down');
const loginSlowDown = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 3,
delayMs: 500, // start with 500ms delay
});
app.post('/login', loginSlowDown, loginRateLimiter, async (req, res) => {
// authentication logic
});This approach makes brute force attacks impractical by exponentially increasing response times after multiple failures.
For distributed applications, use Redis-backed rate limiting:
const Redis = require('ioredis');
const RateLimit = require('express-rate-limit');
const redis = new Redis(process.env.REDIS_URL);
const limiter = new RateLimit({
store: new RateLimit.RedisStore({ redis }),
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many requests from this IP'
});Finally, implement proper error handling that doesn't reveal whether a username exists:
app.post('/login', async (req, res) => {
const { username, password } = req.body;
let shouldLogin = false;
let user;
try {
user = await User.findOne({ username });
if (user) {
shouldLogin = await bcrypt.compare(password, user.password);
}
} catch (err) {
console.error(err);
}
if (shouldLogin) {
req.session.user = user;
return res.json({ success: true });
}
// Always return the same response regardless of failure reason
res.status(401).json({
error: 'Invalid credentials'
});
});This prevents username enumeration, forcing attackers to brute force both username and password simultaneously.