From abf17ef0c863eebfe76e4667d897dccc634188f7 Mon Sep 17 00:00:00 2001 From: ZeroPath Date: Thu, 14 Aug 2025 04:39:54 +0000 Subject: [PATCH 1/2] fix: mitigate brute-force attacks by adding response delay on login failure --- routes/login.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/routes/login.ts b/routes/login.ts index f844def85dd..a9498af235f 100644 --- a/routes/login.ts +++ b/routes/login.ts @@ -50,7 +50,10 @@ module.exports = function login () { // @ts-expect-error FIXME some properties missing in user - vuln-code-snippet hide-line afterLogin(user, res, next) } else { - res.status(401).send(res.__('Invalid email or password.')) + // Delay response to mitigate brute-force attacks + setTimeout(() => { + res.status(401).send(res.__('Invalid email or password.')) + }, 1000) } }).catch((error: Error) => { next(error) From 6fd6408cce923912c274201d039ef0b8feb16e7d Mon Sep 17 00:00:00 2001 From: ZeroPath Date: Thu, 14 Aug 2025 04:40:43 +0000 Subject: [PATCH 2/2] feat: implement rate-limiting and exponential backoff for login attempts --- routes/login.ts | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/routes/login.ts b/routes/login.ts index a9498af235f..12cf66f1dea 100644 --- a/routes/login.ts +++ b/routes/login.ts @@ -16,6 +16,11 @@ import * as utils from '../lib/utils' const security = require('../lib/insecurity') const users = require('../data/datacache').users +const loginAttempts: Map = new Map() +const MAX_ATTEMPTS = 5 +const LOCK_TIME = 15 * 60 * 1000 +const CAPTCHA_THRESHOLD = 3 + // vuln-code-snippet start loginAdminChallenge loginBenderChallenge loginJimChallenge module.exports = function login () { function afterLogin (user: { data: User, bid: number }, res: Response, next: NextFunction) { @@ -50,10 +55,27 @@ module.exports = function login () { // @ts-expect-error FIXME some properties missing in user - vuln-code-snippet hide-line afterLogin(user, res, next) } else { - // Delay response to mitigate brute-force attacks + const key = req.body.email || req.ip + let entry = loginAttempts.get(key) || { attempts: 0, lockUntil: null } + const now = Date.now() + if (entry.lockUntil && entry.lockUntil > now) { + const retryAfter = Math.ceil((entry.lockUntil - now) / 1000) + return res.status(423).json({ status: 'locked', retryAfter }) + } + entry.attempts++ + const delay = Math.min(Math.pow(2, entry.attempts), 32) * 1000 + if (entry.attempts >= MAX_ATTEMPTS) { + entry.lockUntil = now + LOCK_TIME + } + loginAttempts.set(key, entry) + console.warn(`[login] Failed login attempt for ${key} from IP ${req.ip}, attempt ${entry.attempts}`) setTimeout(() => { - res.status(401).send(res.__('Invalid email or password.')) - }, 1000) + res.status(401).json({ + status: 'Invalid email or password.', + captchaRequired: entry.attempts >= CAPTCHA_THRESHOLD, + lock: entry.attempts >= MAX_ATTEMPTS + }) + }, delay) } }).catch((error: Error) => { next(error)