diff --git a/package-lock.json b/package-lock.json index 8402db402..8bf43f4e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "p-limit": "3.1.0", "patch-package": "^6.5.0", "pg": "8.8.0", + "rolling-rate-limiter": "^0.4.2", "swagger-ui-express": "^4.6.2", "tinyld": "^1.3.4", "type-graphql": "^1.2.0-rc.1", @@ -21084,6 +21085,34 @@ "node": ">=8.6" } }, + "node_modules/microtime": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/microtime/-/microtime-3.1.1.tgz", + "integrity": "sha512-to1r7o24cDsud9IhN6/8wGmMx5R2kT0w2Xwm5okbYI3d1dk6Xv0m+Z+jg2vS9pt+ocgQHTCtgs/YuyJhySzxNg==", + "hasInstallScript": true, + "dependencies": { + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.4.0" + }, + "engines": { + "node": ">= 14.13.0" + } + }, + "node_modules/microtime/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/microtime/node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -24637,6 +24666,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rolling-rate-limiter": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/rolling-rate-limiter/-/rolling-rate-limiter-0.4.2.tgz", + "integrity": "sha512-4lKCwwuINmfPvyfLtvAHn+SiwXisH8fmmEvTSHYAenAfKF0p6IErkUEIS+9dvWAslU+97WtX000ne26RAToo2w==", + "dependencies": { + "microtime": "^3.0.0", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/rolling-rate-limiter/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/run": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/run/-/run-1.5.0.tgz", diff --git a/package.json b/package.json index f7fabf4f6..7ee7b7247 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "p-limit": "3.1.0", "patch-package": "^6.5.0", "pg": "8.8.0", + "rolling-rate-limiter": "^0.4.2", "swagger-ui-express": "^4.6.2", "tinyld": "^1.3.4", "type-graphql": "^1.2.0-rc.1", diff --git a/src/auth-server/handlers/registerUserInteraction.ts b/src/auth-server/handlers/registerUserInteraction.ts index 01b89c61a..a6654858e 100644 --- a/src/auth-server/handlers/registerUserInteraction.ts +++ b/src/auth-server/handlers/registerUserInteraction.ts @@ -2,9 +2,16 @@ import express from 'express' import { AuthContext } from '../../utils/auth' import { globalEm } from '../../utils/globalEm' import { components } from '../generated/api-types' -import { UnauthorizedError } from '../errors' +import { TooManyRequestsError, UnauthorizedError } from '../errors' import { UserInteractionCount } from '../../model' +import { InMemoryRateLimiter } from 'rolling-rate-limiter' + +export const interactionLimiter = new InMemoryRateLimiter({ + interval: 1000 * 60 * 5, // 5 minutes + maxInInterval: 1, +}) + type ReqParams = Record type ResBody = | components['schemas']['GenericOkResponseData'] @@ -26,6 +33,12 @@ export const registerUserInteraction: ( throw new UnauthorizedError('Cannot register interactions for empty session') } + const isBlocked = await interactionLimiter.limit(`${type}-${entityId}-${session.userId}`) + + if (isBlocked) { + throw new TooManyRequestsError('Too many requests for single entity') + } + const em = await globalEm await em.transaction(async (em) => { diff --git a/src/auth-server/rateLimits.ts b/src/auth-server/rateLimits.ts index 42ae1632f..28191c150 100644 --- a/src/auth-server/rateLimits.ts +++ b/src/auth-server/rateLimits.ts @@ -32,7 +32,7 @@ export const rateLimitsPerRoute: RateLimitsPerRoute = { '/register-user-interaction': { post: { windowMinutes: 5, - limit: 1, + limit: 30, }, }, '/anonymous-auth': {