Add ip rate limiting

This commit is contained in:
KernelDeimos 2024-04-23 19:13:06 -04:00
parent 65a73b5b45
commit 44aac16991
11 changed files with 146 additions and 4 deletions

View File

@ -192,6 +192,9 @@ const install = async ({ services, app }) => {
const { SessionService } = require('./services/SessionService');
services.registerService('session', SessionService);
const { EdgeRateLimitService } = require('./services/abuse-prevention/EdgeRateLimitService');
services.registerService('edge-rate-limit', EdgeRateLimitService);
}
const install_legacy = async ({ services }) => {

View File

@ -51,6 +51,11 @@ const CHANGE_EMAIL_START = eggspress('/change_email/start', {
key: 'new_email', expected: 'a valid email address' });
}
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('change-email-start') ) {
return res.status(429).send('Too many requests.');
}
// check if email is already in use
const db = req.services.get('database').get(DB_WRITE, 'auth');
const rows = await db.read(
@ -93,6 +98,11 @@ const CHANGE_EMAIL_CONFIRM = eggspress('/change_email/confirm', {
throw APIError.create('field_missing', null, { key: 'token' });
}
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('change-email-confirm') ) {
return res.status(429).send('Too many requests.');
}
const { token, user_id } = jwt.verify(jwt_token, config.jwt_secret);
const db = req.services.get('database').get(DB_WRITE, 'auth');

View File

@ -60,11 +60,10 @@ router.post('/login', express.json(), body_parser_error_handler, async (req, res
else if(req.body.email && !validator.isEmail(req.body.email))
return res.status(400).send('Invalid email.')
// Increment & check rate limit
if(kv.incr(`login|${req.ip}|${req.body.email ?? req.body.username}`) > 10)
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('login') ) {
return res.status(429).send('Too many requests.');
// Set expiry for rate limit
kv.expire(`login|${req.ip}|${req.body.email ?? req.body.username}`, 60*10, 'NX')
}
try{
let user;

View File

@ -45,6 +45,11 @@ router.post('/passwd', auth, express.json(), async (req, res, next)=>{
else if (typeof req.body.new_pass !== 'string')
return res.status(400).send('new_pass must be a string.')
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('passwd') ) {
return res.status(429).send('Too many requests.');
}
try{
// check old_pass
const isMatch = await bcrypt.compare(req.body.old_pass, req.user.password)

View File

@ -70,6 +70,11 @@ router.post('/save_account', auth, express.json(), async (req, res, next)=>{
else if(req.body.password.length < config.min_pass_length)
return res.status(400).send(`Password must be at least ${config.min_pass_length} characters long.`)
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('save-account') ) {
return res.status(429).send('Too many requests.');
}
// duplicate username check, do this only if user has supplied a new username
if(req.body.username !== req.user.username && await username_exists(req.body.username))
return res.status(400).send('This username already exists in our database. Please use another one.');

View File

@ -27,6 +27,11 @@ const { DB_WRITE } = require('../services/database/consts.js');
// POST /send-confirm-email
// -----------------------------------------------------------------------//
router.post('/send-confirm-email', auth, express.json(), async (req, res, next)=>{
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('send-confirm-email') ) {
return res.status(429).send('Too many requests.');
}
// check subdomain
if(require('../helpers').subdomain(req) !== 'api')
next();

View File

@ -51,6 +51,12 @@ router.post('/send-pass-recovery-email', express.json(), body_parser_error_handl
else if(req.body.email && !validator.isEmail(req.body.email))
return res.status(400).send('Invalid email.')
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('send-pass-recovery-email') ) {
return res.status(429).send('Too many requests.');
}
try{
let user;
// see if username exists

View File

@ -52,6 +52,11 @@ router.post('/set-pass-using-token', express.json(), async (req, res, next)=>{
else if(req.body.password.length < config.min_pass_length)
return res.status(400).send(`Password must be at least ${config.min_pass_length} characters long.`)
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('set-pass-using-token') ) {
return res.status(429).send('Too many requests.');
}
try{
const info = await db.write(
'UPDATE user SET password=?, pass_recovery_token=NULL WHERE `uuid` = ? AND pass_recovery_token = ?',

View File

@ -44,6 +44,11 @@ module.exports = eggspress(['/signup'], {
if(require('../helpers').subdomain(req) !== 'api' && require('../helpers').subdomain(req) !== '')
next();
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('signup') ) {
return res.status(429).send('Too many requests.');
}
// modules
const db = req.services.get('database').get(DB_WRITE, 'auth');
const bcrypt = require('bcrypt')

View File

@ -0,0 +1,95 @@
const { Context } = require("../../util/context");
const { asyncSafeSetInterval } = require("../../util/promise");
const { MINUTE, HOUR } = require('../../util/time.js');
const BaseService = require("../BaseService");
class EdgeRateLimitService extends BaseService {
_construct () {
this.scopes = {
['login']: {
limit: 3,
window: 15 * MINUTE,
},
['signup']: {
limit: 10,
window: 15 * MINUTE,
},
['send-confirm-email']: {
limit: 10,
window: HOUR,
},
['send-pass-recovery-email']: {
limit: 10,
window: HOUR,
},
['set-pass-using-token']: {
limit: 10,
window: HOUR,
},
['save-account']: {
limit: 10,
window: HOUR,
},
['change-email-start']: {
limit: 10,
window: HOUR,
},
['change-email-confirm']: {
limit: 10,
window: HOUR,
},
['passwd']: {
limit: 10,
window: HOUR,
},
};
this.requests = new Map();
}
async _init () {
asyncSafeSetInterval(() => this.cleanup(), 5 * MINUTE);
}
check (scope) {
const { window, limit } = this.scopes[scope];
const requester = Context.get('requester');
const rl_identifier = requester.rl_identifier;
const key = `${scope}:${rl_identifier}`;
const now = Date.now();
const windowStart = now - window;
if (!this.requests.has(key)) {
this.requests.set(key, []);
}
// Access the timestamps of past requests for this scope and IP
const timestamps = this.requests.get(key);
// Remove timestamps that are outside the current window
while (timestamps.length > 0 && timestamps[0] < windowStart) {
timestamps.shift();
}
// Check if the current request exceeds the rate limit
if (timestamps.length >= limit) {
return false;
} else {
// Add current timestamp and allow the request
timestamps.push(now);
return true;
}
}
cleanup() {
this.log.tick('edge rate-limit cleanup task');
for (const [key, timestamps] of this.requests.entries()) {
if (timestamps.length === 0) {
this.requests.delete(key);
}
}
}
}
module.exports = { EdgeRateLimitService };

View File

@ -68,6 +68,10 @@ class Requester {
return puter_origins.includes(this.origin);
}
get rl_identifier () {
return this.ip_forwarded || this.ip;
}
serialize () {
return {
ua: this.ua,