[web] add support for regex based allow-list for skipping captcha (#24266)

* [web] double down on validating email addresses

* [web] normalize emails in captcha middleware

* [web] add support for regex based allow-list for skipping captcha

* [web] skip captcha for trusted users on all actions

GitOrigin-RevId: a994ebf6b74e80f462d2dab1fe5113bbffa676a9
This commit is contained in:
Jakob Ackermann
2025-03-18 09:26:17 +00:00
committed by Copybot
parent cc6db7609b
commit 392d2f1c26
4 changed files with 33 additions and 6 deletions

View File

@@ -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

View File

@@ -11,7 +11,7 @@ function getDomain(email) {
}
function parseEmail(email, parseRfcAddress = false) {
if (email == null) {
if (typeof email !== 'string' || !email) {
return null
}

View File

@@ -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,

View File

@@ -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)