From 4af279a72fc9de89ddc3ba51806ca3760a36265d Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Thu, 4 Jul 2024 19:00:21 -0400 Subject: [PATCH] feat: rate-limit for excessive groups --- packages/backend/src/CoreModule.js | 3 ++ .../backend/src/services/AnomalyService.js | 35 +++++++++++++++++++ .../backend/src/services/auth/GroupService.js | 26 +++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/services/AnomalyService.js diff --git a/packages/backend/src/CoreModule.js b/packages/backend/src/CoreModule.js index c04d6ae68..489938ede 100644 --- a/packages/backend/src/CoreModule.js +++ b/packages/backend/src/CoreModule.js @@ -295,6 +295,9 @@ const install = async ({ services, app, useapi }) => { const { MountpointService } = require('./services/MountpointService'); services.registerService('mountpoint', MountpointService); + + const { AnomalyService } = require('./services/AnomalyService'); + services.registerService('anomaly', AnomalyService); } const install_legacy = async ({ services }) => { diff --git a/packages/backend/src/services/AnomalyService.js b/packages/backend/src/services/AnomalyService.js new file mode 100644 index 000000000..a46fbb62f --- /dev/null +++ b/packages/backend/src/services/AnomalyService.js @@ -0,0 +1,35 @@ +const BaseService = require("./BaseService"); + +const DENY_SERVICE_INSTRUCTION = Symbol('DENY_SERVICE_INSTRUCTION'); + +class AnomalyService extends BaseService { + _construct () { + this.types = {}; + } + register (type, config) { + const type_instance = { + config, + } + if ( config.handler ) { + type_instance.handler = config.handler; + } else if ( config.high ) { + type_instance.handler = data => { + if ( data.value > config.high ) { + return new Set([DENY_SERVICE_INSTRUCTION]); + } + } + } + this.types[type] = type_instance; + } + async note (id, data) { + const type = this.types[id]; + if ( ! type ) return; + + return type.handler(data); + } +} + +module.exports = { + AnomalyService, + DENY_SERVICE_INSTRUCTION, +}; diff --git a/packages/backend/src/services/auth/GroupService.js b/packages/backend/src/services/auth/GroupService.js index 2f39bd07e..5694844fe 100644 --- a/packages/backend/src/services/auth/GroupService.js +++ b/packages/backend/src/services/auth/GroupService.js @@ -1,4 +1,6 @@ +const APIError = require("../../api/APIError"); const Group = require("../../entities/Group"); +const { DENY_SERVICE_INSTRUCTION } = require("../AnomalyService"); const BaseService = require("../BaseService"); const { DB_WRITE } = require("../database/consts"); @@ -9,6 +11,11 @@ class GroupService extends BaseService { _init () { this.db = this.services.get('database').get(DB_WRITE, 'permissions'); + + const svc_anomaly = this.services.get('anomaly'); + svc_anomaly.register('groups-user-hour', { + high: 20, + }); } async get({ uid }) { @@ -31,7 +38,24 @@ class GroupService extends BaseService { metadata = metadata ?? {}; const uid = this.modules.uuidv4(); - + + const [{ n_groups }] = await this.db.read( + "SELECT COUNT(*) AS n_groups FROM `group` WHERE " + + "owner_user_id=? AND " + + "created_at >= datetime('now', '-1 hour')", + [owner_user_id] + ); + + const svc_anomaly = this.services.get('anomaly'); + const anomaly = await svc_anomaly.note('groups-user-hour', { + value: n_groups, + user_id: owner_user_id, + }); + + if ( anomaly && anomaly.has(DENY_SERVICE_INSTRUCTION) ) { + throw APIError.create('too_many_requests'); + } + await this.db.write( 'INSERT INTO `group` ' + '(`uid`, `owner_user_id`, `extra`, `metadata`) ' +