From 2fc5e56a6173d78c0fb7360757ea113d26d39002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 5 Jan 2026 22:05:00 +0100 Subject: [PATCH] WIP: Abuse reset on success --- app/controllers/api/account.php | 3 +++ app/controllers/shared/api.php | 38 ++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index d6b99f8855..2c481b500c 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -959,6 +959,7 @@ App::post('/v1/account/sessions/email') )) ->label('abuse-limit', 10) ->label('abuse-key', 'url:{url},email:{param-email}') + ->label('abuse-reset', [201]) ->param('email', '', new EmailValidator(), 'User email.') ->param('password', '', new Password(), 'User password. Must be at least 8 chars.') ->inject('request') @@ -1257,6 +1258,7 @@ App::post('/v1/account/sessions/token') )) ->label('abuse-limit', 10) ->label('abuse-key', 'ip:{ip},userId:{param-userId}') + ->label('abuse-reset', [201]) ->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('secret', '', new Text(256), 'Secret of a token generated by login methods. For example, the `createMagicURLToken` or `createPhoneToken` methods.') ->inject('request') @@ -2645,6 +2647,7 @@ App::put('/v1/account/sessions/magic-url') )) ->label('abuse-limit', 10) ->label('abuse-key', 'ip:{ip},userId:{param-userId}') + ->label('abuse-reset', [201]) ->param('userId', '', new CustomId(), 'User ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('secret', '', new Text(256), 'Valid verification token.') ->inject('request') diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 83b56f626a..05c08a2231 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -814,7 +814,8 @@ App::shutdown() ->inject('queueForWebhooks') ->inject('queueForRealtime') ->inject('dbForProject') - ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject) use ($parseLabel) { + ->inject('timelimit') + ->action(function (App $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, Audit $queueForAudits, StatsUsage $queueForStatsUsage, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, Messaging $queueForMessaging, Func $queueForFunctions, Event $queueForWebhooks, Realtime $queueForRealtime, Database $dbForProject, callable $timelimit) use ($parseLabel) { $responsePayload = $response->getPayload(); @@ -848,6 +849,41 @@ App::shutdown() $route = $utopia->getRoute(); $requestParams = $route->getParamsValues(); + /** + * Abuse labels + */ + $abuseEnabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; + $abuseResetCode = $route->getLabel('abuse-reset', []); + $abuseResetCode = \is_array($abuseResetCode) ? $abuseResetCode : [$abuseResetCode]; + + if ($abuseEnabled && \count($abuseResetCode) > 0 && \in_array($response->getStatusCode(), $abuseResetCode)) { + $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); + $abuseKeyLabel = (!is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; + + foreach ($abuseKeyLabel as $abuseKey) { + $start = $request->getContentRangeStart(); + $end = $request->getContentRangeEnd(); + $timeLimit = $timelimit($abuseKey, $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600)); + $timeLimit + ->setParam('{projectId}', $project->getId()) + ->setParam('{userId}', $user->getId()) + ->setParam('{userAgent}', $request->getUserAgent('')) + ->setParam('{ip}', $request->getIP()) + ->setParam('{url}', $request->getHostname() . $route->getPath()) + ->setParam('{method}', $request->getMethod()) + ->setParam('{chunkId}', (int)($start / ($end + 1 - $start))); + + foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys + if (!empty($value)) { + $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); + } + } + + $abuse = new Abuse($timeLimit); + $abuse->reset(); + } + } + /** * Audit labels */