Password Spraying in Express with Bearer Tokens
Password Spraying in Express with Bearer Tokens — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication attack that attempts a small number of common passwords across many accounts to avoid account lockouts. When Express APIs use bearer tokens for authentication, password spraying can be chained with weak token handling to increase risk. For example, an endpoint that accepts a username and password and returns a bearer token is a natural target: attackers use a list of common passwords and a list of known or guessed usernames, issuing a login request for each combination. If the endpoint does not enforce rate limits per user or globally, an attacker can iterate through passwords without triggering account lockout. A typical vulnerable Express route might look like this:
app.post('/login', (req, res) => {
const { username, password } = req.body;
// naive check — no rate limiting or lockout
if (username === 'alice' && password === 'Password1') {
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGFnIn0.XXXXXXXX';
res.json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
In this pattern, if the attacker knows the username (or enumerates it via user enumeration), they can repeatedly issue login calls with different passwords. Because the route only validates credentials and issues a bearer token without throttling, the attacker can perform password spraying efficiently. The presence of a bearer token does not prevent spraying; it is the authentication logic and lack of protections that create the opening. Additionally, if the token is long-lived or does not rotate on failed attempts, compromised credentials remain useful across multiple requests. The issue is not the bearer token format itself, but the absence of controls around token issuance after authentication.
Another scenario involves endpoints that accept bearer tokens for privileged actions but do not re-authenticate or verify the binding between the token and the requested action. An attacker who obtains a low-privilege token (perhaps through other means) might attempt password spraying on a credential-to-token exchange endpoint to elevate privileges. Because the token is used for authorization rather than authentication, developers may overlook the need to rate-limit the initial credential validation step. This combination of permissive token usage and weak authentication controls makes password spraying a practical attack path in Express APIs that rely on bearer tokens.
Bearer Tokens-Specific Remediation in Express — concrete code fixes
To mitigate password spraying when using bearer tokens in Express, focus on authentication endpoints with rate limiting, account lockout, and secure token issuance. Below is a hardened example that uses express-rate-limit and a small in-memory login attempt tracker to protect against spraying while issuing bearer tokens safely:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 login requests per window
message: { error: 'Too many login attempts, try again later' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/login', limiter);
const loginAttempts = new Map();
const MAX_ATTEMPTS = 5;
const LOCKOUT_MS = 15 * 60 * 1000;
app.post('/login', (req, res) => {
const { username, password } = req.body;
const key = username || req.ip;
const attempts = loginAttempts.get(key) || 0;
if (attempts >= MAX_ATTEMPTS) {
return res.status(429).json({ error: 'Account temporarily locked, try later' });
}
// Replace with secure user lookup and password hashing check
if (username === 'alice' && password === 'Password1') {
loginAttempts.delete(key);
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGFnIn0.XXXXXXXX';
return res.json({ token });
}
loginAttempts.set(key, attempts + 1);
setTimeout(() => loginAttempts.delete(key), LOCKOUT_MS);
res.status(401).json({ error: 'Invalid credentials' });
});
This code enforces rate limiting at the route level and tracks failed attempts per username or IP. After exceeding the maximum attempts, the endpoint returns a 429 to deter spraying. Note that production implementations should use a persistent store for attempts and employ secure password hashing (e.g., bcrypt) rather than plaintext checks. Additionally, ensure bearer tokens are issued with reasonable lifetimes and, where feasible, rotate tokens on authentication to reduce the impact of token leakage.
For token validation across protected routes, avoid trusting the token payload alone without verification. Use a middleware that validates the token signature and checks revocation when necessary. Here is an example of a protected route using bearer tokens safely:
const jwt = require('jsonwebtoken');
const PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\n...
-----END PUBLIC KEY-----';
function authenticateToken(req, res, next) {
const auth = req.headers['authorization'];
const token = auth && auth.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, PUBLIC_KEY, { algorithms: ['RS256'] }, (err, decoded) => {
if (err) return res.sendStatus(403);
req.user = decoded;
next();
});
}
app.get('/profile', authenticateToken, (req, res) => {
res.json({ user: req.user.name });
});
This validation middleware ensures that only properly signed tokens are accepted, reducing the risk of token misuse. Combined with protections on the authentication endpoint, it helps ensure that bearer tokens are issued and used securely within Express APIs.