diff --git a/config/config.sample.php b/config/config.sample.php index 0c25af4d502..e1879fbe5a2 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -463,6 +463,30 @@ $CONFIG = [ */ 'ratelimit.protection.enabled' => true, +/** + * Overwrite the individual rate limit for a specific route + * + * From time to time it can be necessary to extend the rate limit of a specific route, + * depending on your usage pattern or when you script some actions. + * Instead of completely disabling the rate limit or excluding an IP address from the + * rate limit, the following config allows to overwrite the rate limit duration and period. + * + * The first level key is the name of the route. You can find the route name from a URL + * using the ``occ router:list`` command of your server. + * + * You can also specify different limits for logged-in users with the ``user`` key + * and not-logged-in users with the ``anon`` key. However, if there is no specific ``user`` limit, + * the ``anon`` limit is also applied for logged-in users. + * + * Defaults to empty array ``[]`` + */ +'ratelimit_overwrite' => [ + 'profile.profilepage.index' => [ + 'user' => ['limit' => 300, 'period' => 3600], + 'anon' => ['limit' => 1, 'period' => 300], + ] +], + /** * Size of subnet used to normalize IPv6 * diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php index 2d19be97993..906e57a86ef 100644 --- a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php @@ -22,9 +22,11 @@ use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Middleware; use OCP\IAppConfig; +use OCP\IConfig; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; +use Psr\Log\LoggerInterface; use ReflectionMethod; /** @@ -56,7 +58,9 @@ class RateLimitingMiddleware extends Middleware { protected Limiter $limiter, protected ISession $session, protected IAppConfig $appConfig, + protected IConfig $serverConfig, protected BruteforceAllowList $bruteForceAllowList, + protected LoggerInterface $logger, ) { } @@ -74,7 +78,13 @@ class RateLimitingMiddleware extends Middleware { } if ($this->userSession->isLoggedIn()) { - $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class); + $rateLimit = $this->readLimitFromAnnotationOrAttribute( + $controller, + $methodName, + 'UserRateThrottle', + UserRateLimit::class, + 'user', + ); if ($rateLimit !== null) { if ($this->appConfig->getValueBool('bruteforcesettings', 'apply_allowlist_to_ratelimit') @@ -94,7 +104,13 @@ class RateLimitingMiddleware extends Middleware { // If not user specific rate limit is found the Anon rate limit applies! } - $rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'AnonRateThrottle', AnonRateLimit::class); + $rateLimit = $this->readLimitFromAnnotationOrAttribute( + $controller, + $methodName, + 'AnonRateThrottle', + AnonRateLimit::class, + 'anon', + ); if ($rateLimit !== null) { $this->limiter->registerAnonRequest( @@ -115,7 +131,35 @@ class RateLimitingMiddleware extends Middleware { * @param class-string $attributeClass * @return ?ARateLimit */ - protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass): ?ARateLimit { + protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass, string $overwriteKey): ?ARateLimit { + $rateLimitOverwrite = $this->serverConfig->getSystemValue('ratelimit_overwrite', []); + if (!empty($rateLimitOverwrite)) { + $controllerRef = new \ReflectionClass($controller); + $appName = $controllerRef->getProperty('appName')->getValue($controller); + $controllerName = substr($controller::class, strrpos($controller::class, '\\') + 1); + $controllerName = substr($controllerName, 0, 0 - strlen('Controller')); + + $overwriteConfig = strtolower($appName . '.' . $controllerName . '.' . $methodName); + $rateLimitOverwriteForActionAndType = $rateLimitOverwrite[$overwriteConfig][$overwriteKey] ?? null; + if ($rateLimitOverwriteForActionAndType !== null) { + $isValid = isset($rateLimitOverwriteForActionAndType['limit'], $rateLimitOverwriteForActionAndType['period']) + && $rateLimitOverwriteForActionAndType['limit'] > 0 + && $rateLimitOverwriteForActionAndType['period'] > 0; + + if ($isValid) { + return new $attributeClass( + (int)$rateLimitOverwriteForActionAndType['limit'], + (int)$rateLimitOverwriteForActionAndType['period'], + ); + } + + $this->logger->warning('Rate limit overwrite on controller "{overwriteConfig}" for "{overwriteKey}" is invalid', [ + 'overwriteConfig' => $overwriteConfig, + 'overwriteKey' => $overwriteKey, + ]); + } + } + $annotationLimit = $this->reflector->getAnnotationParameter($annotationName, 'limit'); $annotationPeriod = $this->reflector->getAnnotationParameter($annotationName, 'period'); diff --git a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php index c42baadcb1c..358d24fa4ec 100644 --- a/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/RateLimitingMiddlewareTest.php @@ -20,11 +20,13 @@ use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\IAppConfig; +use OCP\IConfig; use OCP\IRequest; use OCP\ISession; use OCP\IUser; use OCP\IUserSession; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; use Test\TestCase; class TestRateLimitController extends Controller { @@ -64,7 +66,9 @@ class RateLimitingMiddlewareTest extends TestCase { private Limiter|MockObject $limiter; private ISession|MockObject $session; private IAppConfig|MockObject $appConfig; + private IConfig|MockObject $serverConfig; private BruteforceAllowList|MockObject $bruteForceAllowList; + private LoggerInterface|MockObject $logger; private RateLimitingMiddleware $rateLimitingMiddleware; protected function setUp(): void { @@ -76,7 +80,9 @@ class RateLimitingMiddlewareTest extends TestCase { $this->limiter = $this->createMock(Limiter::class); $this->session = $this->createMock(ISession::class); $this->appConfig = $this->createMock(IAppConfig::class); + $this->serverConfig = $this->createMock(IConfig::class); $this->bruteForceAllowList = $this->createMock(BruteforceAllowList::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->rateLimitingMiddleware = new RateLimitingMiddleware( $this->request, @@ -85,7 +91,9 @@ class RateLimitingMiddlewareTest extends TestCase { $this->limiter, $this->session, $this->appConfig, + $this->serverConfig, $this->bruteForceAllowList, + $this->logger ); }