From 3fe81069debc99633202df0b6f13cc12ac54ec7e Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 11 May 2026 18:50:35 +0200 Subject: [PATCH 1/2] feat: allow apps to selectivally apply rate limiting Signed-off-by: Robin Appelman --- .../Middleware/Security/RateLimitingMiddleware.php | 10 +++++++++- lib/public/AppFramework/Http/Attribute/ARateLimit.php | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php index 8a7ef6df1460c..4fffc982492f3 100644 --- a/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/RateLimitingMiddleware.php @@ -89,6 +89,10 @@ public function beforeController(Controller $controller, string $methodName): vo ); if ($rateLimit !== null) { + if (!$rateLimit->shouldApply($this->request)) { + return; + } + if ($this->appConfig->getValueBool('bruteforcesettings', 'apply_allowlist_to_ratelimit') && $this->bruteForceAllowList->isBypassListed($this->request->getRemoteAddress())) { return; @@ -115,6 +119,10 @@ public function beforeController(Controller $controller, string $methodName): vo ); if ($rateLimit !== null) { + if (!$rateLimit->shouldApply($this->request)) { + return; + } + $this->limiter->registerAnonRequest( $rateLimitIdentifier, $rateLimit->getLimit(), @@ -174,7 +182,7 @@ protected function readLimitFromAnnotationOrAttribute(Controller $controller, st } $reflectionMethod = new ReflectionMethod($controller, $methodName); - $attributes = $reflectionMethod->getAttributes($attributeClass); + $attributes = $reflectionMethod->getAttributes($attributeClass, \ReflectionAttribute::IS_INSTANCEOF); $attribute = current($attributes); if ($attribute !== false) { diff --git a/lib/public/AppFramework/Http/Attribute/ARateLimit.php b/lib/public/AppFramework/Http/Attribute/ARateLimit.php index c06b1180ae3b3..db66204847607 100644 --- a/lib/public/AppFramework/Http/Attribute/ARateLimit.php +++ b/lib/public/AppFramework/Http/Attribute/ARateLimit.php @@ -9,6 +9,8 @@ namespace OCP\AppFramework\Http\Attribute; +use OCP\IRequest; + /** * Attribute for controller methods that want to limit the times a logged-in * user can call the endpoint in a given time period. @@ -40,4 +42,11 @@ public function getLimit(): int { public function getPeriod(): int { return $this->period; } + + /** + * @since 35.0.0 + */ + public function shouldApply(IRequest $request): bool { + return true; + } } From a97fe9d2ae7592c41cc8edc6da7aa6eafcb62db7 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Mon, 11 May 2026 19:04:19 +0200 Subject: [PATCH 2/2] feat: add rate limiting for creating federated shares for non-trusted servers Signed-off-by: Robin Appelman --- .../Controller/RequestHandlerController.php | 2 + .../Controller/RequestHandlerController.php | 3 +- lib/composer/composer/autoload_classmap.php | 1 + lib/composer/composer/autoload_static.php | 1 + .../Http/Attributes/FederationRateLimit.php | 55 +++++++++++++++++++ 5 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 lib/private/AppFramework/Http/Attributes/FederationRateLimit.php diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index a5af02af2ad2d..0a9bda68189e8 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -7,6 +7,7 @@ namespace OCA\CloudFederationAPI\Controller; +use OC\AppFramework\Http\Attributes\FederationRateLimit; use OC\Authentication\Token\PublicKeyTokenProvider; use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; @@ -107,6 +108,7 @@ public function __construct( #[PublicPage] #[NoCSRFRequired] #[BruteForceProtection(action: 'receiveFederatedShare')] + #[FederationRateLimit(limit: 5, period: 1200)] public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { try { diff --git a/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php index 219aabaecb599..e420741fbb49f 100644 --- a/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php +++ b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php @@ -8,6 +8,7 @@ namespace OCA\FederatedFileSharing\Controller; +use OC\AppFramework\Http\Attributes\FederationRateLimit; use OCA\FederatedFileSharing\FederatedShareProvider; use OCP\App\IAppManager; use OCP\AppFramework\Http; @@ -30,7 +31,6 @@ use OCP\IRequest; use OCP\Log\Audit\CriticalActionPerformedEvent; use OCP\Server; -use OCP\Share; use OCP\Share\Exceptions\ShareNotFound; use Psr\Log\LoggerInterface; @@ -70,6 +70,7 @@ public function __construct( */ #[NoCSRFRequired] #[PublicPage] + #[FederationRateLimit(limit: 5, period: 1200)] public function createShare( ?string $remote = null, ?string $token = null, diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 1e741f5a116b4..030046d24d60e 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1127,6 +1127,7 @@ 'OC\\AppFramework\\Bootstrap\\ServiceRegistration' => $baseDir . '/lib/private/AppFramework/Bootstrap/ServiceRegistration.php', 'OC\\AppFramework\\DependencyInjection\\DIContainer' => $baseDir . '/lib/private/AppFramework/DependencyInjection/DIContainer.php', 'OC\\AppFramework\\Http' => $baseDir . '/lib/private/AppFramework/Http.php', + 'OC\\AppFramework\\Http\\Attributes\\FederationRateLimit' => $baseDir . '/lib/private/AppFramework/Http/Attributes/FederationRateLimit.php', 'OC\\AppFramework\\Http\\Attributes\\TwoFactorSetUpDoneRequired' => $baseDir . '/lib/private/AppFramework/Http/Attributes/TwoFactorSetUpDoneRequired.php', 'OC\\AppFramework\\Http\\Dispatcher' => $baseDir . '/lib/private/AppFramework/Http/Dispatcher.php', 'OC\\AppFramework\\Http\\Output' => $baseDir . '/lib/private/AppFramework/Http/Output.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index e12aefe665741..7d9b979f6c943 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1168,6 +1168,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\AppFramework\\Bootstrap\\ServiceRegistration' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Bootstrap/ServiceRegistration.php', 'OC\\AppFramework\\DependencyInjection\\DIContainer' => __DIR__ . '/../../..' . '/lib/private/AppFramework/DependencyInjection/DIContainer.php', 'OC\\AppFramework\\Http' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http.php', + 'OC\\AppFramework\\Http\\Attributes\\FederationRateLimit' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Attributes/FederationRateLimit.php', 'OC\\AppFramework\\Http\\Attributes\\TwoFactorSetUpDoneRequired' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Attributes/TwoFactorSetUpDoneRequired.php', 'OC\\AppFramework\\Http\\Dispatcher' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Dispatcher.php', 'OC\\AppFramework\\Http\\Output' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Http/Output.php', diff --git a/lib/private/AppFramework/Http/Attributes/FederationRateLimit.php b/lib/private/AppFramework/Http/Attributes/FederationRateLimit.php new file mode 100644 index 0000000000000..98930adceca48 --- /dev/null +++ b/lib/private/AppFramework/Http/Attributes/FederationRateLimit.php @@ -0,0 +1,55 @@ +discoveryService = Server::get(OCMDiscoveryService::class); + $this->trustedServers = Server::get(TrustedServers::class); + } + + #[\Override] + public function shouldApply(IRequest $request): bool { + if ($this->trustedServers === null) { + return true; + } + + try { + $signedRequest = $this->discoveryService->getIncomingSignedRequest(); + if (!$signedRequest) { + return true; + } + $signedRequest->verify(); + return !$this->trustedServers->isTrustedServer($signedRequest->getOrigin()); + } catch (\Exception) { + // no or invalid signature + return true; + } + } +}