diff --git a/services/web/app/src/Features/Captcha/CaptchaMiddleware.js b/services/web/app/src/Features/Captcha/CaptchaMiddleware.js index f796749f85..9c93b74cfa 100644 --- a/services/web/app/src/Features/Captcha/CaptchaMiddleware.js +++ b/services/web/app/src/Features/Captcha/CaptchaMiddleware.js @@ -6,6 +6,7 @@ const OError = require('@overleaf/o-error') const DeviceHistory = require('./DeviceHistory') const AuthenticationController = require('../Authentication/AuthenticationController') const { expressify } = require('@overleaf/promise-utils') +const EmailsHelper = require('../Helpers/EmailHelper') function respondInvalidCaptcha(req, res) { res.status(400).json({ @@ -41,9 +42,11 @@ async function canSkipCaptcha(req, res) { function validateCaptcha(action) { return expressify(async function (req, res, next) { + const email = EmailsHelper.parseEmail(req.body?.email) const trustedUser = - req.body?.email && - Settings.recaptcha.trustedUsers.includes(req.body.email) + email && + (Settings.recaptcha.trustedUsers.includes(email) || + Settings.recaptcha.trustedUsersRegex?.test(email)) if (!Settings.recaptcha?.siteKey || Settings.recaptcha.disabled[action]) { if (action === 'login') { AuthenticationController.setAuditInfo(req, { captcha: 'disabled' }) @@ -51,15 +54,17 @@ function validateCaptcha(action) { Metrics.inc('captcha', 1, { path: action, status: 'disabled' }) return next() } - if (trustedUser && action === 'login') { - AuthenticationController.setAuditInfo(req, { captcha: 'trusted' }) + if (trustedUser) { + if (action === 'login') { + AuthenticationController.setAuditInfo(req, { captcha: 'trusted' }) + } Metrics.inc('captcha', 1, { path: action, status: 'trusted' }) return next() } const reCaptchaResponse = req.body['g-recaptcha-response'] if (action === 'login') { await initializeDeviceHistory(req) - const fromKnownDevice = req.deviceHistory.has(req.body?.email) + const fromKnownDevice = req.deviceHistory.has(email) AuthenticationController.setAuditInfo(req, { fromKnownDevice }) if (!reCaptchaResponse && fromKnownDevice) { // The user has previously logged in from this device, which required diff --git a/services/web/app/src/Features/Helpers/EmailHelper.js b/services/web/app/src/Features/Helpers/EmailHelper.js index c24e68876f..80b96dfea0 100644 --- a/services/web/app/src/Features/Helpers/EmailHelper.js +++ b/services/web/app/src/Features/Helpers/EmailHelper.js @@ -11,7 +11,7 @@ function getDomain(email) { } function parseEmail(email, parseRfcAddress = false) { - if (email == null) { + if (typeof email !== 'string' || !email) { return null } diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 8f9910a8f0..54255dffa9 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -779,6 +779,10 @@ module.exports = { .split(',') .map(x => x.trim()) .filter(x => x !== ''), + trustedUsersRegex: process.env.CAPTCHA_TRUSTED_USERS_REGEX + ? // Enforce matching of the entire input. + new RegExp(`^${process.env.CAPTCHA_TRUSTED_USERS_REGEX}$`) + : null, disabled: { invite: true, login: true, diff --git a/services/web/test/unit/src/HelperFiles/EmailHelperTests.js b/services/web/test/unit/src/HelperFiles/EmailHelperTests.js index 762ca60759..2d56b7c638 100644 --- a/services/web/test/unit/src/HelperFiles/EmailHelperTests.js +++ b/services/web/test/unit/src/HelperFiles/EmailHelperTests.js @@ -18,6 +18,24 @@ describe('EmailHelper', function () { expect(parseEmail(address, true)).to.equal(expected) }) + it('should return null for garbage input', function () { + const cases = [ + undefined, + null, + '', + 42, + ['test@example.com'], + {}, + { length: 42 }, + { trim: true, match: true }, + { toString: true }, + ] + for (const input of cases) { + expect(parseEmail(input)).to.equal(null, input) + expect(parseEmail(input, true)).to.equal(null, input) + } + }) + it('should return null for an invalid single email', function () { const address = 'testexample.com' expect(parseEmail(address)).to.equal(null)