BruteForceMiddleware.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\AppFramework\Middleware\Security;
  8. use OC\AppFramework\Utility\ControllerMethodReflector;
  9. use OCP\AppFramework\Controller;
  10. use OCP\AppFramework\Http;
  11. use OCP\AppFramework\Http\Attribute\BruteForceProtection;
  12. use OCP\AppFramework\Http\Response;
  13. use OCP\AppFramework\Http\TooManyRequestsResponse;
  14. use OCP\AppFramework\Middleware;
  15. use OCP\AppFramework\OCS\OCSException;
  16. use OCP\AppFramework\OCSController;
  17. use OCP\IRequest;
  18. use OCP\Security\Bruteforce\IThrottler;
  19. use OCP\Security\Bruteforce\MaxDelayReached;
  20. use Psr\Log\LoggerInterface;
  21. use ReflectionMethod;
  22. /**
  23. * Class BruteForceMiddleware performs the bruteforce protection for controllers
  24. * that are annotated with @BruteForceProtection(action=$action) whereas $action
  25. * is the action that should be logged within the database.
  26. *
  27. * @package OC\AppFramework\Middleware\Security
  28. */
  29. class BruteForceMiddleware extends Middleware {
  30. private int $delaySlept = 0;
  31. public function __construct(
  32. protected ControllerMethodReflector $reflector,
  33. protected IThrottler $throttler,
  34. protected IRequest $request,
  35. protected LoggerInterface $logger,
  36. ) {
  37. }
  38. /**
  39. * {@inheritDoc}
  40. */
  41. public function beforeController($controller, $methodName) {
  42. parent::beforeController($controller, $methodName);
  43. if ($this->reflector->hasAnnotation('BruteForceProtection')) {
  44. $action = $this->reflector->getAnnotationParameter('BruteForceProtection', 'action');
  45. $this->delaySlept += $this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), $action);
  46. } else {
  47. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  48. $attributes = $reflectionMethod->getAttributes(BruteForceProtection::class);
  49. if (!empty($attributes)) {
  50. $remoteAddress = $this->request->getRemoteAddress();
  51. foreach ($attributes as $attribute) {
  52. /** @var BruteForceProtection $protection */
  53. $protection = $attribute->newInstance();
  54. $action = $protection->getAction();
  55. $this->delaySlept += $this->throttler->sleepDelayOrThrowOnMax($remoteAddress, $action);
  56. }
  57. }
  58. }
  59. }
  60. /**
  61. * {@inheritDoc}
  62. */
  63. public function afterController($controller, $methodName, Response $response) {
  64. if ($response->isThrottled()) {
  65. try {
  66. if ($this->reflector->hasAnnotation('BruteForceProtection')) {
  67. $action = $this->reflector->getAnnotationParameter('BruteForceProtection', 'action');
  68. $ip = $this->request->getRemoteAddress();
  69. $this->throttler->registerAttempt($action, $ip, $response->getThrottleMetadata());
  70. $this->delaySlept += $this->throttler->sleepDelayOrThrowOnMax($ip, $action);
  71. } else {
  72. $reflectionMethod = new ReflectionMethod($controller, $methodName);
  73. $attributes = $reflectionMethod->getAttributes(BruteForceProtection::class);
  74. if (!empty($attributes)) {
  75. $ip = $this->request->getRemoteAddress();
  76. $metaData = $response->getThrottleMetadata();
  77. foreach ($attributes as $attribute) {
  78. /** @var BruteForceProtection $protection */
  79. $protection = $attribute->newInstance();
  80. $action = $protection->getAction();
  81. if (!isset($metaData['action']) || $metaData['action'] === $action) {
  82. $this->throttler->registerAttempt($action, $ip, $metaData);
  83. $this->delaySlept += $this->throttler->sleepDelayOrThrowOnMax($ip, $action);
  84. }
  85. }
  86. } else {
  87. $this->logger->debug('Response for ' . get_class($controller) . '::' . $methodName . ' got bruteforce throttled but has no annotation nor attribute defined.');
  88. }
  89. }
  90. } catch (MaxDelayReached $e) {
  91. if ($controller instanceof OCSController) {
  92. throw new OCSException($e->getMessage(), Http::STATUS_TOO_MANY_REQUESTS);
  93. }
  94. return new TooManyRequestsResponse();
  95. }
  96. }
  97. if ($this->delaySlept) {
  98. $response->addHeader('X-Nextcloud-Bruteforce-Throttled', $this->delaySlept . 'ms');
  99. }
  100. return parent::afterController($controller, $methodName, $response);
  101. }
  102. /**
  103. * @param Controller $controller
  104. * @param string $methodName
  105. * @param \Exception $exception
  106. * @throws \Exception
  107. * @return Response
  108. */
  109. public function afterException($controller, $methodName, \Exception $exception): Response {
  110. if ($exception instanceof MaxDelayReached) {
  111. if ($controller instanceof OCSController) {
  112. throw new OCSException($exception->getMessage(), Http::STATUS_TOO_MANY_REQUESTS);
  113. }
  114. return new TooManyRequestsResponse();
  115. }
  116. throw $exception;
  117. }
  118. }