From ac79a91aa9d8696c5738a88e7fef12c29f7ad866 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 12 May 2026 13:12:13 +0530 Subject: [PATCH 1/8] Fail open on abuse timelimit failures --- app/controllers/shared/api.php | 45 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 8365274e98..6a63fab3a6 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -555,27 +555,38 @@ Http::init() } $abuse = new Abuse($timeLimit); - $remaining = $timeLimit->remaining(); - - $limit = $timeLimit->limit(); - $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600); - - if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) { - $closestLimit = $remaining; - $response - ->addHeader('X-RateLimit-Limit', $limit) - ->addHeader('X-RateLimit-Remaining', $remaining) - ->addHeader('X-RateLimit-Reset', $time); - } $enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; + $shouldCheckAbuse = $enabled + && ! $isAppUser + && ! $isPrivilegedUser + && $devKey->isEmpty(); + $isRateLimited = false; + + try { + $remaining = $timeLimit->remaining(); + + $limit = $timeLimit->limit(); + $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600); + + if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) { + $closestLimit = $remaining; + $response + ->addHeader('X-RateLimit-Limit', $limit) + ->addHeader('X-RateLimit-Remaining', $remaining) + ->addHeader('X-RateLimit-Reset', $time); + } + + if ($shouldCheckAbuse) { + $isRateLimited = $abuse->check(); + } + } catch (\Throwable) { + continue; + } if ( - $enabled // Abuse is enabled - && ! $isAppUser // User is not API key - && ! $isPrivilegedUser // User is not an admin - && $devKey->isEmpty() // request doesn't not contain development key - && $abuse->check() // Route is rate-limited + $shouldCheckAbuse // Abuse is enabled and the user is rate-limited + && $isRateLimited // Route is rate-limited ) { throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED); } From 728298d2f5f8f643c4b60bba25fbdbb888aa9ef5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 12 May 2026 13:23:39 +0530 Subject: [PATCH 2/8] Address abuse timelimit review comments --- app/controllers/shared/api.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 6a63fab3a6..7b7d394908 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -546,6 +546,11 @@ Http::init() $roles = $authorization->getRoles(); $isPrivilegedUser = $user->isPrivileged($roles); $isAppUser = $user->isApp($roles); + $enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; + $shouldCheckAbuse = $enabled + && ! $isAppUser + && ! $isPrivilegedUser + && $devKey->isEmpty(); foreach ($timeLimitArray as $timeLimit) { foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys @@ -556,11 +561,6 @@ Http::init() $abuse = new Abuse($timeLimit); - $enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; - $shouldCheckAbuse = $enabled - && ! $isAppUser - && ! $isPrivilegedUser - && $devKey->isEmpty(); $isRateLimited = false; try { @@ -585,7 +585,7 @@ Http::init() } if ( - $shouldCheckAbuse // Abuse is enabled and the user is rate-limited + $shouldCheckAbuse // Abuse is enabled and caller is not privileged/app/dev && $isRateLimited // Route is rate-limited ) { throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED); From d523c7425aefc2dcddf990cf0d016573da007a9d Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 12 May 2026 13:31:33 +0530 Subject: [PATCH 3/8] Log abuse timelimit fail-open errors --- app/controllers/shared/api.php | 7 ++++-- app/init/resources/request.php | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 7b7d394908..923b878c44 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -499,8 +499,9 @@ Http::init() ->inject('telemetry') ->inject('platform') ->inject('authorization') + ->inject('logError') ->inject('cacheControlForStorage') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $logError, callable $cacheControlForStorage) { $response->setUser($user); $request->setUser($user); @@ -580,7 +581,9 @@ Http::init() if ($shouldCheckAbuse) { $isRateLimited = $abuse->check(); } - } catch (\Throwable) { + } catch (\Throwable $th) { + \call_user_func($logError, $th, 'http', 'api.abuse.timelimit'); + continue; } diff --git a/app/init/resources/request.php b/app/init/resources/request.php index 6ed377d9ae..a090b5523a 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -34,6 +34,7 @@ use Utopia\Auth\Proofs\Token; use Utopia\Auth\Store; use Utopia\Cache\Cache; use Utopia\Config\Config; +use Utopia\Console; use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\DateTime as DatabaseDateTime; @@ -48,6 +49,7 @@ use Utopia\Locale\Locale; use Utopia\Logger\Log; use Utopia\Pools\Group; use Utopia\Queue\Publisher; +use Utopia\Registry\Registry; use Utopia\Storage\Device; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -70,6 +72,49 @@ return function (Container $container): void { return $register->get('logger'); }, ['register']); + $container->set('logError', function (Registry $register, Request $request, Document $project, Authorization $authorization) { + return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $request, $project, $authorization) { + $logger = $register->get('logger'); + + if (!$logger) { + return; + } + + $log = new Log(); + $log->setNamespace($namespace); + $log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname())); + $log->setVersion(System::getEnv('_APP_VERSION', 'UNKNOWN')); + $log->setType(Log::TYPE_ERROR); + $log->setMessage($error->getMessage()); + $log->setAction($action); + + $log->addTag('code', $error->getCode()); + $log->addTag('verboseType', \get_class($error)); + $log->addTag('projectId', $project->getId()); + $log->addTag('method', $request->getMethod()); + $log->addTag('url', $request->getURI()); + + $log->addExtra('file', $error->getFile()); + $log->addExtra('line', $error->getLine()); + $log->addExtra('trace', $error->getTraceAsString()); + $log->addExtra('roles', $authorization->getRoles()); + + foreach (($extras ?? []) as $key => $value) { + $log->addExtra($key, $value); + } + + $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; + $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); + + try { + $responseCode = $logger->addLog($log); + Console::info('Error log pushed with status code: ' . $responseCode); + } catch (Throwable $th) { + Console::error('Error pushing log: ' . $th->getMessage()); + } + }; + }, ['register', 'request', 'project', 'authorization']); + $container->set('authorization', function () { return new Authorization(); }, []); From 7914fde9f12a2a04a03e652bd093a6b2c13cf8cd Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 12 May 2026 16:09:20 +0530 Subject: [PATCH 4/8] Use error_log for abuse timelimit failures --- app/controllers/shared/api.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 923b878c44..1dcf05e3f9 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -499,9 +499,8 @@ Http::init() ->inject('telemetry') ->inject('platform') ->inject('authorization') - ->inject('logError') ->inject('cacheControlForStorage') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $logError, callable $cacheControlForStorage) { + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { $response->setUser($user); $request->setUser($user); @@ -582,7 +581,7 @@ Http::init() $isRateLimited = $abuse->check(); } } catch (\Throwable $th) { - \call_user_func($logError, $th, 'http', 'api.abuse.timelimit'); + \error_log((string) $th); continue; } From a51fd49ed1911e0870535c535845d64d2bd22974 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 12 May 2026 16:17:23 +0530 Subject: [PATCH 5/8] Refactor abuse rate limiting init hook --- app/controllers/shared/api.php | 161 +++++++++++++++++---------------- 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 1dcf05e3f9..7127cceeef 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -476,6 +476,86 @@ Http::init() } }); +Http::init() + ->groups(['api']) + ->inject('utopia') + ->inject('request') + ->inject('response') + ->inject('project') + ->inject('user') + ->inject('timelimit') + ->inject('devKey') + ->inject('authorization') + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, callable $timelimit, Document $devKey, Authorization $authorization) { + $response->setUser($user); + $request->setUser($user); + + if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'disabled') { + return; + } + + $roles = $authorization->getRoles(); + if ($user->isApp($roles) || $user->isPrivileged($roles) || ! $devKey->isEmpty()) { + return; + } + + $route = $utopia->getRoute(); + if ($route === null) { + throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND); + } + + $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); + $abuseKeyLabel = (! is_array($abuseKeyLabel)) ? [$abuseKeyLabel] : $abuseKeyLabel; + $closestLimit = null; + + 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) { + if (! empty($value)) { + $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); + } + } + + $abuse = new Abuse($timeLimit); + $isRateLimited = false; + + try { + $remaining = $timeLimit->remaining(); + $limit = $timeLimit->limit(); + $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600); + + if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) { + $closestLimit = $remaining; + $response + ->addHeader('X-RateLimit-Limit', $limit) + ->addHeader('X-RateLimit-Remaining', $remaining) + ->addHeader('X-RateLimit-Reset', $time); + } + + $isRateLimited = $abuse->check(); + } catch (\Throwable $th) { + \error_log((string) $th); + + continue; + } + + if ($isRateLimited) { + throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED); + } + } + }); + Http::init() ->groups(['api']) ->inject('utopia') @@ -490,17 +570,15 @@ Http::init() ->inject('usage') ->inject('queueForFunctions') ->inject('dbForProject') - ->inject('timelimit') ->inject('resourceToken') ->inject('mode') ->inject('apiKey') ->inject('plan') - ->inject('devKey') ->inject('telemetry') ->inject('platform') ->inject('authorization') ->inject('cacheControlForStorage') - ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { + ->action(function (Http $utopia, Request $request, Response $response, Document $project, User $user, Event $queueForEvents, AuditContext $auditContext, Delete $queueForDeletes, EventDatabase $queueForDatabase, Context $usage, Func $queueForFunctions, Database $dbForProject, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Telemetry $telemetry, array $platform, Authorization $authorization, callable $cacheControlForStorage) { $response->setUser($user); $request->setUser($user); @@ -517,83 +595,6 @@ Http::init() default => '', }; - /* - * Abuse Check - */ - - $abuseKeyLabel = $route->getLabel('abuse-key', 'url:{url},ip:{ip}'); - $timeLimitArray = []; - - $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))); - $timeLimitArray[] = $timeLimit; - } - - $closestLimit = null; - - $roles = $authorization->getRoles(); - $isPrivilegedUser = $user->isPrivileged($roles); - $isAppUser = $user->isApp($roles); - $enabled = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled'; - $shouldCheckAbuse = $enabled - && ! $isAppUser - && ! $isPrivilegedUser - && $devKey->isEmpty(); - - foreach ($timeLimitArray as $timeLimit) { - 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); - - $isRateLimited = false; - - try { - $remaining = $timeLimit->remaining(); - - $limit = $timeLimit->limit(); - $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600); - - if ($limit && ($remaining < $closestLimit || is_null($closestLimit))) { - $closestLimit = $remaining; - $response - ->addHeader('X-RateLimit-Limit', $limit) - ->addHeader('X-RateLimit-Remaining', $remaining) - ->addHeader('X-RateLimit-Reset', $time); - } - - if ($shouldCheckAbuse) { - $isRateLimited = $abuse->check(); - } - } catch (\Throwable $th) { - \error_log((string) $th); - - continue; - } - - if ( - $shouldCheckAbuse // Abuse is enabled and caller is not privileged/app/dev - && $isRateLimited // Route is rate-limited - ) { - throw new Exception(Exception::GENERAL_RATE_LIMIT_EXCEEDED); - } - } - /** * TODO: (@loks0n) * Avoid mutating the message across file boundaries - it's difficult to reason about at scale. From e1327845324a3a239358891459e71cb50582d500 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 12 May 2026 16:24:50 +0530 Subject: [PATCH 6/8] Fix cache auth role lookup after rate limit refactor --- app/controllers/shared/api.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 7127cceeef..2df8d2167f 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -636,8 +636,10 @@ Http::init() $storageCacheOperationsCounter = $telemetry->createCounter('storage.cache.operations.load'); if ($useCache) { $route = $utopia->match($request); + $roles = $authorization->getRoles(); + $isAppUser = $user->isApp($roles); $isImageTransformation = $route->getPath() === '/v1/storage/buckets/:bucketId/files/:fileId/preview'; - $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($authorization->getRoles()); + $isDisabled = isset($plan['imageTransformations']) && $plan['imageTransformations'] === -1 && ! $user->isPrivileged($roles); $key = $request->cacheIdentifier(); Span::add('storage.cache.key', $key); @@ -658,7 +660,7 @@ Http::init() $bucket = $authorization->skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isToken = ! $resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence(); - $isPrivilegedUser = $user->isPrivileged($authorization->getRoles()); + $isPrivilegedUser = $user->isPrivileged($roles); if ($bucket->isEmpty() || (! $bucket->getAttribute('enabled') && ! $isAppUser && ! $isPrivilegedUser)) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); From 1ca82829443fe137974fb92903bc85f61aac95be Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Tue, 12 May 2026 16:30:31 +0530 Subject: [PATCH 7/8] Remove unused request logError resource --- app/init/resources/request.php | 45 ---------------------------------- 1 file changed, 45 deletions(-) diff --git a/app/init/resources/request.php b/app/init/resources/request.php index a090b5523a..6ed377d9ae 100644 --- a/app/init/resources/request.php +++ b/app/init/resources/request.php @@ -34,7 +34,6 @@ use Utopia\Auth\Proofs\Token; use Utopia\Auth\Store; use Utopia\Cache\Cache; use Utopia\Config\Config; -use Utopia\Console; use Utopia\Database\Adapter\Pool as DatabasePool; use Utopia\Database\Database; use Utopia\Database\DateTime as DatabaseDateTime; @@ -49,7 +48,6 @@ use Utopia\Locale\Locale; use Utopia\Logger\Log; use Utopia\Pools\Group; use Utopia\Queue\Publisher; -use Utopia\Registry\Registry; use Utopia\Storage\Device; use Utopia\System\System; use Utopia\Telemetry\Adapter as Telemetry; @@ -72,49 +70,6 @@ return function (Container $container): void { return $register->get('logger'); }, ['register']); - $container->set('logError', function (Registry $register, Request $request, Document $project, Authorization $authorization) { - return function (Throwable $error, string $namespace, string $action, ?array $extras = null) use ($register, $request, $project, $authorization) { - $logger = $register->get('logger'); - - if (!$logger) { - return; - } - - $log = new Log(); - $log->setNamespace($namespace); - $log->setServer(System::getEnv('_APP_LOGGING_SERVICE_IDENTIFIER', \gethostname())); - $log->setVersion(System::getEnv('_APP_VERSION', 'UNKNOWN')); - $log->setType(Log::TYPE_ERROR); - $log->setMessage($error->getMessage()); - $log->setAction($action); - - $log->addTag('code', $error->getCode()); - $log->addTag('verboseType', \get_class($error)); - $log->addTag('projectId', $project->getId()); - $log->addTag('method', $request->getMethod()); - $log->addTag('url', $request->getURI()); - - $log->addExtra('file', $error->getFile()); - $log->addExtra('line', $error->getLine()); - $log->addExtra('trace', $error->getTraceAsString()); - $log->addExtra('roles', $authorization->getRoles()); - - foreach (($extras ?? []) as $key => $value) { - $log->addExtra($key, $value); - } - - $isProduction = System::getEnv('_APP_ENV', 'development') === 'production'; - $log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING); - - try { - $responseCode = $logger->addLog($log); - Console::info('Error log pushed with status code: ' . $responseCode); - } catch (Throwable $th) { - Console::error('Error pushing log: ' . $th->getMessage()); - } - }; - }, ['register', 'request', 'project', 'authorization']); - $container->set('authorization', function () { return new Authorization(); }, []); From d9987dd645d1fe81e3e3a96d51fd09f4bb8b68a5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Wed, 13 May 2026 15:00:51 +0530 Subject: [PATCH 8/8] Fix rate limit headers when abuse check is skipped --- app/controllers/shared/api.php | 53 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 2df8d2167f..b43ad517a5 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -490,14 +490,11 @@ Http::init() $response->setUser($user); $request->setUser($user); - if (System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') === 'disabled') { - return; - } - $roles = $authorization->getRoles(); - if ($user->isApp($roles) || $user->isPrivileged($roles) || ! $devKey->isEmpty()) { - return; - } + $shouldCheckAbuse = System::getEnv('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled' + && ! $user->isApp($roles) + && ! $user->isPrivileged($roles) + && $devKey->isEmpty(); $route = $utopia->getRoute(); if ($route === null) { @@ -509,28 +506,28 @@ Http::init() $closestLimit = null; 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) { - if (! empty($value)) { - $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); - } - } - - $abuse = new Abuse($timeLimit); $isRateLimited = false; try { + $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) { + if (! empty($value)) { + $timeLimit->setParam('{param-' . $key . '}', (\is_array($value)) ? \json_encode($value) : $value); + } + } + + $abuse = new Abuse($timeLimit); $remaining = $timeLimit->remaining(); $limit = $timeLimit->limit(); $time = $timeLimit->time() + $route->getLabel('abuse-time', 3600); @@ -543,7 +540,9 @@ Http::init() ->addHeader('X-RateLimit-Reset', $time); } - $isRateLimited = $abuse->check(); + if ($shouldCheckAbuse) { + $isRateLimited = $abuse->check(); + } } catch (\Throwable $th) { \error_log((string) $th);