Send "429 Too Many Requests" in case of brute force protection

Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
Joas Schilling 2020-03-19 12:09:57 +01:00
parent 4ff492a492
commit e66bc4a8a7
No known key found for this signature in database
GPG Key ID: 7076EA9751AACDDA
5 changed files with 133 additions and 2 deletions

4
core/templates/429.php Normal file
View File

@ -0,0 +1,4 @@
<div class="body-login-container update">
<h2><?php p($l->t('Too many requests')); ?></h2>
<p class="infogroup"><?php p($l->t('There were too many requests from your network. Retry later or contact your administrator if this is an error.')); ?></p>
</div>

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
*
@ -26,9 +27,15 @@ namespace OC\AppFramework\Middleware\Security;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Security\Bruteforce\Throttler;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TooManyRequestsResponse;
use OCP\AppFramework\Middleware;
use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use OCP\Security\Bruteforce\MaxDelayReached;
/**
* Class BruteForceMiddleware performs the bruteforce protection for controllers
@ -66,7 +73,7 @@ class BruteForceMiddleware extends Middleware {
if ($this->reflector->hasAnnotation('BruteForceProtection')) {
$action = $this->reflector->getAnnotationParameter('BruteForceProtection', 'action');
$this->throttler->sleepDelay($this->request->getRemoteAddress(), $action);
$this->throttler->sleepDelayOrThrowOnMax($this->request->getRemoteAddress(), $action);
}
}
@ -83,4 +90,23 @@ class BruteForceMiddleware extends Middleware {
return parent::afterController($controller, $methodName, $response);
}
/**
* @param Controller $controller
* @param string $methodName
* @param \Exception $exception
* @throws \Exception
* @return Response
*/
public function afterException($controller, $methodName, \Exception $exception): Response {
if ($exception instanceof MaxDelayReached) {
if ($controller instanceof OCSController) {
throw new OCSException($exception->getMessage(), Http::STATUS_TOO_MANY_REQUESTS);
}
return new TooManyRequestsResponse();
}
throw $exception;
}
}

View File

@ -34,6 +34,7 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\ILogger;
use OCP\Security\Bruteforce\MaxDelayReached;
/**
* Class Throttler implements the bruteforce protection for security actions in
@ -50,6 +51,7 @@ use OCP\ILogger;
*/
class Throttler {
public const LOGIN_ACTION = 'login';
public const MAX_DELAY = 25;
/** @var IDBConnection */
private $db;
@ -241,7 +243,7 @@ class Throttler {
return 0;
}
$maxDelay = 25;
$maxDelay = self::MAX_DELAY;
$firstDelay = 0.1;
if ($attempts > (8 * PHP_INT_SIZE - 1)) {
// Don't ever overflow. Just assume the maxDelay time:s
@ -308,4 +310,22 @@ class Throttler {
usleep($delay * 1000);
return $delay;
}
/**
* Will sleep for the defined amount of time unless maximum is reached
* In case of maximum a "429 Too Many Request" response is thrown
*
* @param string $ip
* @param string $action optionally filter by action
* @return int the time spent sleeping
* @throws MaxDelayReached when reached the maximum
*/
public function sleepDelayOrThrowOnMax($ip, $action = '') {
$delay = $this->getDelay($ip, $action);
if ($delay === self::MAX_DELAY * 1000) {
throw new MaxDelayReached();
}
usleep($delay * 1000);
return $delay;
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\AppFramework\Http;
use OCP\Template;
/**
* A generic 429 response showing an 404 error page as well to the end-user
* @since 19.0.0
*/
class TooManyRequestsResponse extends Response {
/**
* @since 19.0.0
*/
public function __construct() {
parent::__construct();
$this->setContentSecurityPolicy(new ContentSecurityPolicy());
$this->setStatus(429);
}
/**
* @return string
* @since 19.0.0
*/
public function render() {
$template = new Template('core', '429', 'blank');
return $template->fetchPage();
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\Security\Bruteforce;
/**
* Class MaxDelayReached
* @since 19.0
*/
class MaxDelayReached extends \RuntimeException {
}