From e66bc4a8a74ad6071569ea707e986a0e21aca66d Mon Sep 17 00:00:00 2001
From: Joas Schilling <coding@schilljs.com>
Date: Thu, 19 Mar 2020 12:09:57 +0100
Subject: [PATCH] Send "429 Too Many Requests" in case of brute force
 protection

Signed-off-by: Joas Schilling <coding@schilljs.com>
---
 core/templates/429.php                        |  4 ++
 .../Security/BruteForceMiddleware.php         | 28 +++++++++-
 lib/private/Security/Bruteforce/Throttler.php | 22 +++++++-
 .../Http/TooManyRequestsResponse.php          | 51 +++++++++++++++++++
 .../Security/Bruteforce/MaxDelayReached.php   | 30 +++++++++++
 5 files changed, 133 insertions(+), 2 deletions(-)
 create mode 100644 core/templates/429.php
 create mode 100644 lib/public/AppFramework/Http/TooManyRequestsResponse.php
 create mode 100644 lib/public/Security/Bruteforce/MaxDelayReached.php

diff --git a/core/templates/429.php b/core/templates/429.php
new file mode 100644
index 00000000000..caf8a3675e2
--- /dev/null
+++ b/core/templates/429.php
@@ -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>
diff --git a/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php b/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php
index 398c2f3f3a4..e9b03266462 100644
--- a/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/BruteForceMiddleware.php
@@ -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;
+	}
 }
diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php
index 63c6361b9ce..8e485046602 100644
--- a/lib/private/Security/Bruteforce/Throttler.php
+++ b/lib/private/Security/Bruteforce/Throttler.php
@@ -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;
+	}
 }
diff --git a/lib/public/AppFramework/Http/TooManyRequestsResponse.php b/lib/public/AppFramework/Http/TooManyRequestsResponse.php
new file mode 100644
index 00000000000..160614ab33e
--- /dev/null
+++ b/lib/public/AppFramework/Http/TooManyRequestsResponse.php
@@ -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();
+	}
+}
diff --git a/lib/public/Security/Bruteforce/MaxDelayReached.php b/lib/public/Security/Bruteforce/MaxDelayReached.php
new file mode 100644
index 00000000000..817ef0e60c3
--- /dev/null
+++ b/lib/public/Security/Bruteforce/MaxDelayReached.php
@@ -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 {
+}
-- 
GitLab