Files
overleaf-cep/services/web/app/src/Features/Security/RateLimiterMiddleware.js
T
Antoine Clausse 5f2718cf29 [web] Make rate-limit on login consistent, prevent "trim/case bypass" (#19555)
* Replace `LoginRateLimiter.processLoginRequest` call by use of `RateLimiterMiddleware`

* Lowercase the email to avoid rate-limit bypass

* Remove unit test "when the users rate limit"

* Use `EmailHelper.parseEmail` to normalize email in `processLoginRequest`

This should address the `trim()` bypass

* Use `.trim().toLowerCase()` instead of `EmailHelper.parseEmail`

We can't use `EmailHelper.parseEmail`, else it breaks the test (and feature): "with username that does not look like an email"

* Add acceptance test for rate limit

* Add comment on rate limits

* Rename `rateLimiter` to `rateLimiterLoginEmail` for clarity

* Make the login rate limits configurable from the settings

GitOrigin-RevId: cf1c3a416745f2b007c85014a5084570d4a049a7
2024-07-30 08:04:26 +00:00

90 lines
2.3 KiB
JavaScript

const logger = require('@overleaf/logger')
const SessionManager = require('../Authentication/SessionManager')
const LoginRateLimiter = require('./LoginRateLimiter')
const settings = require('@overleaf/settings')
/**
* Return a rate limiting middleware
*
* Pass an array of opts.params to segment this based on parameters in the
* request URL, e.g.:
*
* app.get "/project/:project_id", RateLimiterMiddleware.rateLimit(
* rateLimiter, params: ["project_id"]
* )
*
* will rate limit each project_id separately.
*
* Unique clients are identified by user_id if logged in, and IP address if not.
* The method label is used to identify this in our metrics.
*/
function rateLimit(rateLimiter, opts = {}) {
const getUserId =
opts.getUserId || (req => SessionManager.getLoggedInUserId(req.session))
return function (req, res, next) {
const clientId = opts.ipOnly ? req.ip : getUserId(req) || req.ip
const method = clientId === req.ip ? 'ip' : 'userId'
if (
settings.smokeTest &&
settings.smokeTest.userId &&
settings.smokeTest.userId.toString() === clientId.toString()
) {
// ignore smoke test user
return next()
}
let key = clientId
if (!opts.ipOnly) {
const params = (opts.params || []).map(p => req.params[p])
params.push(clientId)
key = params.join(':')
}
rateLimiter
.consume(key, 1, { method })
.then(() => next())
.catch(err => {
if (err instanceof Error) {
next(err)
} else {
res.status(429) // Too many requests
res.write('Rate limit reached, please try again later')
res.end()
}
})
}
}
function loginRateLimitEmail(req, res, next) {
const { email } = req.body
if (!email) {
return next()
}
LoginRateLimiter.processLoginRequest(email, (err, isAllowed) => {
if (err) {
return next(err)
}
if (isAllowed) {
next()
} else {
logger.warn({ email }, 'rate limit exceeded')
res.status(429) // Too many requests
res.json({
message: {
type: 'error',
text: req.i18n.translate('to_many_login_requests_2_mins'),
key: 'to-many-login-requests-2-mins',
},
})
}
})
}
const RateLimiterMiddleware = {
rateLimit,
loginRateLimitEmail,
}
module.exports = RateLimiterMiddleware