Description
Password checks or authentication middleware alone do not limit repeated login attempts. Without controls such as attempt limits, delays, locks or CAPTCHA, attackers may repeatedly try passwords. Track and limit attempts regardless of whether authentication succeeds.
Potential impact
- Accounts may be exposed to brute-force or credential-stuffing attacks.
- Untracked failures may delay attack detection and response.
Remediation
- Track failed logins by account, IP address and device as appropriate.
- Apply limits to login endpoints with middleware such as
express-rate-limit. - Record lock and unlock events in security logs and alerts.
Examples
These route excerpts omit body parsing, rateLimit initialization, user lookup and error handling. Configure trusted proxy IP handling, IPv6 address grouping and shared counters across application instances for the deployment.
Before
javascript
app.post("/login", async (req, res) => {
const user = await loadUser(req.body.username);
const ok = await bcrypt.compare(req.body.password, user.passwordHash);
res.json({ ok });
});
After
javascript
const loginLimiter = rateLimit({
windowMs: 60_000,
max: 5,
keyGenerator: (req) => `${req.ip}:${req.body.username ?? ""}`,
});
app.post("/login", loginLimiter, async (req, res) => {
const user = await loadUser(req.body.username);
const ok = await bcrypt.compare(req.body.password, user.passwordHash);
res.json({ ok });
});
Explanation:
- Before: Password verification runs without an attempt limit, so repeated guesses can reach the authentication handler.
- After: The limiter permits five requests per minute for the same IP-and-username pair. It does not count only failures or implement account locks or device limits. Add separate account or IP limits to address attempts that change one of those values.