Protecting Login Endpoints from Credential Stuffing
Credential stuffing uses username/password pairs leaked from other sites, tried automatically against yours. Because many people reuse passwords, even a small success rate means many hijacked accounts.
1. Layered rate limits
import rateLimit from 'express-rate-limit';
// Per IP: slows down a single machine
app.post('/login', rateLimit({ windowMs: 15 * 60_000, limit: 20 }), loginHandler);
// Per account: slows down distributed attacks on one user
async function tooManyFailures(email) {
const failures = await redis.incr(`login:fail:${email.toLowerCase()}`);
await redis.expire(`login:fail:${email.toLowerCase()}`, 15 * 60);
return failures > 10;
}
Attackers rotate through thousands of IPs, so per-IP limits alone aren't enough. Watch global failure rates too, and tighten checks when they spike.
2. Prefer slowing down to locking out
Hard lockouts let attackers lock real users out on purpose. Increasing delays, a CAPTCHA after several failures, or a step-up check (email code) protect accounts without handing attackers a denial-of-service tool.
3. Block known-breached passwords
import { createHash } from 'node:crypto';
// k-anonymity: only the first 5 characters of the SHA-1 hash leave your server
async function isBreached(password) {
const hash = createHash('sha1').update(password).digest('hex').toUpperCase();
const res = await fetch(`https://api.pwnedpasswords.com/range/${hash.slice(0, 5)}`);
const body = await res.text();
return body.split('\n').some((line) => line.startsWith(hash.slice(5)));
}
Check at sign-up and password change, and prompt users with a breached password to change it.
4. Multi-factor authentication
MFA (ideally passkeys) makes a correct password insufficient on its own. Encourage it for everyone and require it for admins.
5. Detect and respond
- Alert on spikes in failed logins and on logins from unusual locations or devices.
- Notify users of new sign-ins and offer "this wasn't me" with session revocation.
- Use bot protection at the edge (WAF rules, bot scoring) for high-value login pages.
6. Don't help attackers
Use the same error for "no such user" and "wrong password", and keep response times similar, so attackers can't confirm which emails have accounts.
Comments
No comments yet. Have a question or a tip? Be the first to share it.