From d4499b169bb5b0929bbcc47cb1c4e33fc21e3353 Mon Sep 17 00:00:00 2001 From: ZeroPath Date: Thu, 14 Aug 2025 05:00:12 +0000 Subject: [PATCH 1/2] fix: prevent email enumeration by standardizing response shape --- routes/securityQuestion.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/routes/securityQuestion.ts b/routes/securityQuestion.ts index f780e8acf18..345a957c3e1 100644 --- a/routes/securityQuestion.ts +++ b/routes/securityQuestion.ts @@ -19,7 +19,8 @@ module.exports = function securityQuestion () { }).then((answer: SecurityAnswerModel | null) => { if (answer != null) { SecurityQuestionModel.findByPk(answer.SecurityQuestionId).then((question: SecurityQuestionModel | null) => { - res.json({ question }) + // Always return the same shape to prevent email enumeration + res.json({}) }).catch((error: Error) => { next(error) }) From e6ce44272c007fbc5206aa2cf8b0ebae9f12e811 Mon Sep 17 00:00:00 2001 From: ZeroPath Date: Thu, 14 Aug 2025 05:00:49 +0000 Subject: [PATCH 2/2] fix: Implement CAPTCHA and rate limiting to enhance security measures --- routes/securityQuestion.ts | 44 ++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/routes/securityQuestion.ts b/routes/securityQuestion.ts index 345a957c3e1..bbe32dbcee3 100644 --- a/routes/securityQuestion.ts +++ b/routes/securityQuestion.ts @@ -8,9 +8,42 @@ import { SecurityAnswerModel } from '../models/securityAnswer' import { UserModel } from '../models/user' import { SecurityQuestionModel } from '../models/securityQuestion' +const requestCounts: Record = {} + +function verifyCaptcha(req: Request): boolean { + const token = req.headers['x-captcha-token'] + return typeof token === 'string' && token.length > 0 +} + +function rateLimiter(req: Request, res: Response): boolean { + const ip = req.ip + let entry = requestCounts[ip] + if (!entry) { + entry = { count: 0, resetTime: Date.now() + 60 * 60 * 1000 } + } + if (Date.now() > entry.resetTime) { + entry.count = 0 + entry.resetTime = Date.now() + 60 * 60 * 1000 + } + entry.count++ + requestCounts[ip] = entry + if (entry.count > 10) { + res.status(429).json({ error: 'Too many requests' }) + return false + } + return true +} + module.exports = function securityQuestion () { - return ({ query }: Request, res: Response, next: NextFunction) => { - const email = query.email + return (req: Request, res: Response, next: NextFunction) => { + if (!verifyCaptcha(req)) { + res.status(400).json({ error: 'Invalid CAPTCHA' }) + return + } + if (!rateLimiter(req, res)) { + return + } + const email = req.query.email SecurityAnswerModel.findOne({ include: [{ model: UserModel, @@ -19,12 +52,15 @@ module.exports = function securityQuestion () { }).then((answer: SecurityAnswerModel | null) => { if (answer != null) { SecurityQuestionModel.findByPk(answer.SecurityQuestionId).then((question: SecurityQuestionModel | null) => { - // Always return the same shape to prevent email enumeration - res.json({}) + // Return actual security question and set resetRequest cookie + res.cookie('resetRequest', answer.SecurityQuestionId.toString(), { httpOnly: true }) + res.json({ question: question?.question || '' }) }).catch((error: Error) => { next(error) }) } else { + // Dummy cookie to prevent enumeration and maintain consistent flow + res.cookie('resetRequest', 'dummy', { httpOnly: true }) res.json({}) } }).catch((error: unknown) => {