aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLukas Reschke <lukas@statuscode.ch>2016-07-20 18:36:15 +0200
committerLukas Reschke <lukas@statuscode.ch>2016-07-20 22:08:56 +0200
commitba4f12baa02dfb55ec8822687896d643261440c4 (patch)
tree5dc95ab54a2ae169951693a43ba7aa6920d6f36a
parent7cdf6402ff9a0e07866ca8bcfcffd0e0897b646a (diff)
downloadnextcloud-server-ba4f12baa02dfb55ec8822687896d643261440c4.tar.gz
nextcloud-server-ba4f12baa02dfb55ec8822687896d643261440c4.zip
Implement brute force protection
Class Throttler implements the bruteforce protection for security actions in Nextcloud. It is working by logging invalid login attempts to the database and slowing down all login attempts from the same subnet. The max delay is 30 seconds and the starting delay are 200 milliseconds. (after the first failed login)
-rw-r--r--apps/dav/appinfo/v1/caldav.php1
-rw-r--r--apps/dav/appinfo/v1/carddav.php1
-rw-r--r--apps/dav/appinfo/v1/webdav.php1
-rw-r--r--apps/dav/lib/Connector/Sabre/Auth.php9
-rw-r--r--apps/dav/lib/Server.php3
-rw-r--r--apps/dav/tests/unit/Connector/Sabre/AuthTest.php9
-rwxr-xr-xbuild/integration/run.sh3
-rw-r--r--config/config.sample.php7
-rw-r--r--core/Application.php3
-rw-r--r--core/Controller/LoginController.php27
-rw-r--r--core/Controller/TokenController.php18
-rw-r--r--db_structure.xml78
-rw-r--r--lib/base.php2
-rw-r--r--lib/private/AppFramework/DependencyInjection/DIContainer.php3
-rw-r--r--lib/private/AppFramework/Middleware/Security/CORSMiddleware.php25
-rw-r--r--lib/private/Security/Bruteforce/Throttler.php230
-rw-r--r--lib/private/Server.php16
-rw-r--r--lib/private/User/Session.php23
-rw-r--r--lib/private/legacy/api.php2
-rw-r--r--tests/Core/Controller/LoginControllerTest.php59
-rw-r--r--tests/lib/AppFramework/DependencyInjection/DIContainerTest.php4
-rw-r--r--tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php28
-rw-r--r--tests/lib/Security/Bruteforce/ThrottlerTest.php123
-rw-r--r--tests/lib/User/SessionTest.php46
-rw-r--r--version.php2
25 files changed, 655 insertions, 68 deletions
diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php
index 50348a60202..975fd34ae8e 100644
--- a/apps/dav/appinfo/v1/caldav.php
+++ b/apps/dav/appinfo/v1/caldav.php
@@ -35,6 +35,7 @@ $authBackend = new Auth(
\OC::$server->getUserSession(),
\OC::$server->getRequest(),
\OC::$server->getTwoFactorAuthManager(),
+ \OC::$server->getBruteForceThrottler(),
'principals/'
);
$principalBackend = new Principal(
diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php
index fc7aff4a63c..e2d8944fcb6 100644
--- a/apps/dav/appinfo/v1/carddav.php
+++ b/apps/dav/appinfo/v1/carddav.php
@@ -36,6 +36,7 @@ $authBackend = new Auth(
\OC::$server->getUserSession(),
\OC::$server->getRequest(),
\OC::$server->getTwoFactorAuthManager(),
+ \OC::$server->getBruteForceThrottler(),
'principals/'
);
$principalBackend = new Principal(
diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php
index 3b733c0fbd5..2af49177ce1 100644
--- a/apps/dav/appinfo/v1/webdav.php
+++ b/apps/dav/appinfo/v1/webdav.php
@@ -43,6 +43,7 @@ $authBackend = new \OCA\DAV\Connector\Sabre\Auth(
\OC::$server->getUserSession(),
\OC::$server->getRequest(),
\OC::$server->getTwoFactorAuthManager(),
+ \OC::$server->getBruteForceThrottler(),
'principals/'
);
$requestUri = \OC::$server->getRequest()->getRequestUri();
diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php
index 28e4ae2bcde..3f9e16b04c5 100644
--- a/apps/dav/lib/Connector/Sabre/Auth.php
+++ b/apps/dav/lib/Connector/Sabre/Auth.php
@@ -33,6 +33,7 @@ use Exception;
use OC\AppFramework\Http\Request;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
use OC\Authentication\TwoFactorAuth\Manager;
+use OC\Security\Bruteforce\Throttler;
use OC\User\Session;
use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden;
use OCP\IRequest;
@@ -58,23 +59,28 @@ class Auth extends AbstractBasic {
private $currentUser;
/** @var Manager */
private $twoFactorManager;
+ /** @var Throttler */
+ private $throttler;
/**
* @param ISession $session
* @param Session $userSession
* @param IRequest $request
* @param Manager $twoFactorManager
+ * @param Throttler $throttler
* @param string $principalPrefix
*/
public function __construct(ISession $session,
Session $userSession,
IRequest $request,
Manager $twoFactorManager,
+ Throttler $throttler,
$principalPrefix = 'principals/users/') {
$this->session = $session;
$this->userSession = $userSession;
$this->twoFactorManager = $twoFactorManager;
$this->request = $request;
+ $this->throttler = $throttler;
$this->principalPrefix = $principalPrefix;
// setup realm
@@ -107,6 +113,7 @@ class Auth extends AbstractBasic {
* @param string $username
* @param string $password
* @return bool
+ * @throws PasswordLoginForbidden
*/
protected function validateUserPass($username, $password) {
if ($this->userSession->isLoggedIn() &&
@@ -118,7 +125,7 @@ class Auth extends AbstractBasic {
} else {
\OC_Util::setupFS(); //login hooks may need early access to the filesystem
try {
- if ($this->userSession->logClientIn($username, $password, $this->request)) {
+ if ($this->userSession->logClientIn($username, $password, $this->request, $this->throttler)) {
\OC_Util::setupFS($this->userSession->getUser()->getUID());
$this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID());
$this->session->close();
diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php
index 0715d39049c..c0cb5ecd62d 100644
--- a/apps/dav/lib/Server.php
+++ b/apps/dav/lib/Server.php
@@ -64,7 +64,8 @@ class Server {
\OC::$server->getSession(),
\OC::$server->getUserSession(),
\OC::$server->getRequest(),
- \OC::$server->getTwoFactorAuthManager()
+ \OC::$server->getTwoFactorAuthManager(),
+ \OC::$server->getBruteForceThrottler()
);
// Set URL explicitly due to reverse-proxy situations
diff --git a/apps/dav/tests/unit/Connector/Sabre/AuthTest.php b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php
index 92798797d6c..142b83a45b8 100644
--- a/apps/dav/tests/unit/Connector/Sabre/AuthTest.php
+++ b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php
@@ -28,6 +28,7 @@
namespace OCA\DAV\Tests\unit\Connector\Sabre;
use OC\Authentication\TwoFactorAuth\Manager;
+use OC\Security\Bruteforce\Throttler;
use OC\User\Session;
use OCP\IRequest;
use OCP\ISession;
@@ -51,6 +52,8 @@ class AuthTest extends TestCase {
private $request;
/** @var Manager */
private $twoFactorManager;
+ /** @var Throttler */
+ private $throttler;
public function setUp() {
parent::setUp();
@@ -63,11 +66,15 @@ class AuthTest extends TestCase {
$this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
->disableOriginalConstructor()
->getMock();
+ $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->auth = new \OCA\DAV\Connector\Sabre\Auth(
$this->session,
$this->userSession,
$this->request,
- $this->twoFactorManager
+ $this->twoFactorManager,
+ $this->throttler
);
}
diff --git a/build/integration/run.sh b/build/integration/run.sh
index 2abceaa1fad..eccb378eec4 100755
--- a/build/integration/run.sh
+++ b/build/integration/run.sh
@@ -9,6 +9,9 @@ else
exit 1
fi
+# Disable bruteforce protection because the integration tests do trigger them
+../../occ config:system:set auth.bruteforce.protection.enabled --value false --type bool
+
composer install
SCENARIO_TO_RUN=$1
diff --git a/config/config.sample.php b/config/config.sample.php
index 051e5422fe5..c9f5fecf5f9 100644
--- a/config/config.sample.php
+++ b/config/config.sample.php
@@ -208,6 +208,13 @@ $CONFIG = array(
'token_auth_enforced' => false,
/**
+ * Whether the bruteforce protection shipped with Nextcloud should be enabled or not.
+ *
+ * Disabling this is discouraged for security reasons.
+ */
+'auth.bruteforce.protection.enabled' => true,
+
+/**
* The directory where the skeleton files are located. These files will be
* copied to the data directory of new users. Leave empty to not copy any
* skeleton files.
diff --git a/core/Application.php b/core/Application.php
index 1485f7a7516..82ec5ad023c 100644
--- a/core/Application.php
+++ b/core/Application.php
@@ -103,7 +103,8 @@ class Application extends App {
$c->query('Session'),
$c->query('UserSession'),
$c->query('URLGenerator'),
- $c->query('TwoFactorAuthManager')
+ $c->query('TwoFactorAuthManager'),
+ $c->query('ServerContainer')->getBruteforceThrottler()
);
});
$container->registerService('TwoFactorChallengeController', function (SimpleContainer $c) {
diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php
index 7806e1de904..c453bd20a23 100644
--- a/core/Controller/LoginController.php
+++ b/core/Controller/LoginController.php
@@ -22,7 +22,9 @@
namespace OC\Core\Controller;
+use OC\AppFramework\Utility\TimeFactory;
use OC\Authentication\TwoFactorAuth\Manager;
+use OC\Security\Bruteforce\Throttler;
use OC\User\Session;
use OC_App;
use OC_Util;
@@ -37,24 +39,20 @@ use OCP\IUser;
use OCP\IUserManager;
class LoginController extends Controller {
-
/** @var IUserManager */
private $userManager;
-
/** @var IConfig */
private $config;
-
/** @var ISession */
private $session;
-
/** @var Session */
private $userSession;
-
/** @var IURLGenerator */
private $urlGenerator;
-
/** @var Manager */
private $twoFactorManager;
+ /** @var Throttler */
+ private $throttler;
/**
* @param string $appName
@@ -65,9 +63,17 @@ class LoginController extends Controller {
* @param Session $userSession
* @param IURLGenerator $urlGenerator
* @param Manager $twoFactorManager
+ * @param Throttler $throttler
*/
- function __construct($appName, IRequest $request, IUserManager $userManager, IConfig $config, ISession $session,
- Session $userSession, IURLGenerator $urlGenerator, Manager $twoFactorManager) {
+ function __construct($appName,
+ IRequest $request,
+ IUserManager $userManager,
+ IConfig $config,
+ ISession $session,
+ Session $userSession,
+ IURLGenerator $urlGenerator,
+ Manager $twoFactorManager,
+ Throttler $throttler) {
parent::__construct($appName, $request);
$this->userManager = $userManager;
$this->config = $config;
@@ -75,6 +81,7 @@ class LoginController extends Controller {
$this->userSession = $userSession;
$this->urlGenerator = $urlGenerator;
$this->twoFactorManager = $twoFactorManager;
+ $this->throttler = $throttler;
}
/**
@@ -171,6 +178,8 @@ class LoginController extends Controller {
* @return RedirectResponse
*/
public function tryLogin($user, $password, $redirect_url) {
+ $this->throttler->sleepDelay($this->request->getRemoteAddress());
+
$originalUser = $user;
// TODO: Add all the insane error handling
/* @var $loginResult IUser */
@@ -184,6 +193,8 @@ class LoginController extends Controller {
}
}
if ($loginResult === false) {
+ $this->throttler->registerAttempt('login', $this->request->getRemoteAddress(), ['user' => $originalUser]);
+
$this->session->set('loginMessages', [
['invalidpassword']
]);
diff --git a/core/Controller/TokenController.php b/core/Controller/TokenController.php
index 13b1db9044a..8401c4f23a9 100644
--- a/core/Controller/TokenController.php
+++ b/core/Controller/TokenController.php
@@ -1,5 +1,4 @@
<?php
-
/**
* @author Christoph Wurst <christoph@owncloud.com>
*
@@ -23,6 +22,7 @@
namespace OC\Core\Controller;
use OC\AppFramework\Http;
+use OC\AppFramework\Utility\TimeFactory;
use OC\Authentication\Token\DefaultTokenProvider;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\IToken;
@@ -35,27 +35,29 @@ use OCP\IRequest;
use OCP\Security\ISecureRandom;
class TokenController extends Controller {
-
/** @var UserManager */
private $userManager;
-
/** @var IProvider */
private $tokenProvider;
-
/** @var TwoFactorAuthManager */
private $twoFactorAuthManager;
-
/** @var ISecureRandom */
private $secureRandom;
/**
* @param string $appName
* @param IRequest $request
- * @param Manager $userManager
- * @param DefaultTokenProvider $tokenProvider
+ * @param UserManager $userManager
+ * @param IProvider $tokenProvider
+ * @param TwoFactorAuthManager $twoFactorAuthManager
* @param ISecureRandom $secureRandom
*/
- public function __construct($appName, IRequest $request, UserManager $userManager, IProvider $tokenProvider, TwoFactorAuthManager $twoFactorAuthManager, ISecureRandom $secureRandom) {
+ public function __construct($appName,
+ IRequest $request,
+ UserManager $userManager,
+ IProvider $tokenProvider,
+ TwoFactorAuthManager $twoFactorAuthManager,
+ ISecureRandom $secureRandom) {
parent::__construct($appName, $request);
$this->userManager = $userManager;
$this->tokenProvider = $tokenProvider;
diff --git a/db_structure.xml b/db_structure.xml
index 1127f0d82d4..04c91ea494f 100644
--- a/db_structure.xml
+++ b/db_structure.xml
@@ -1166,6 +1166,84 @@
<table>
<!--
+ List of usernames, their display name and login password.
+ -->
+ <name>*dbprefix*bruteforce_attempts</name>
+
+ <declaration>
+ <field>
+ <name>id</name>
+ <type>integer</type>
+ <default>0</default>
+ <notnull>true</notnull>
+ <autoincrement>1</autoincrement>
+ <unsigned>true</unsigned>
+ <length>4</length>
+ </field>
+
+ <field>
+ <name>action</name>
+ <type>text</type>
+ <default></default>
+ <notnull>true</notnull>
+ <length>64</length>
+ </field>
+
+ <field>
+ <name>occurred</name>
+ <type>integer</type>
+ <default>0</default>
+ <notnull>true</notnull>
+ <unsigned>true</unsigned>
+ <length>4</length>
+ </field>
+
+ <field>
+ <name>ip</name>
+ <type>text</type>
+ <default></default>
+ <notnull>true</notnull>
+ <length>255</length>
+ </field>
+
+ <field>
+ <name>subnet</name>
+ <type>text</type>
+ <default></default>
+ <notnull>true</notnull>
+ <length>255</length>
+ </field>
+
+ <field>
+ <name>metadata</name>
+ <type>text</type>
+ <default></default>
+ <notnull>true</notnull>
+ <length>255</length>
+ </field>
+
+ <index>
+ <name>bruteforce_attempts_ip</name>
+ <field>
+ <name>ip</name>
+ <sorting>ascending</sorting>
+ </field>
+ </index>
+ <index>
+ <name>bruteforce_attempts_subnet</name>
+ <field>
+ <name>subnet</name>
+ <sorting>ascending</sorting>
+ </field>
+ </index>
+
+ </declaration>
+
+ </table>
+
+ <table>
+
+ <!--
List of tags (category) + a unique tag id (id) per user (uid) and type.
-->
<name>*dbprefix*vcategory</name>
diff --git a/lib/base.php b/lib/base.php
index be9de93f73f..a5e9d9716b7 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -913,7 +913,7 @@ class OC {
if ($userSession->tryTokenLogin($request)) {
return true;
}
- if ($userSession->tryBasicAuthLogin($request)) {
+ if ($userSession->tryBasicAuthLogin($request, \OC::$server->getBruteForceThrottler())) {
return true;
}
return false;
diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php
index f21b34a6b4a..1684ff8027b 100644
--- a/lib/private/AppFramework/DependencyInjection/DIContainer.php
+++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php
@@ -351,7 +351,8 @@ class DIContainer extends SimpleContainer implements IAppContainer {
return new CORSMiddleware(
$c['Request'],
$c['ControllerMethodReflector'],
- $c['OCP\IUserSession']
+ $c['OCP\IUserSession'],
+ $c->getServer()->getBruteForceThrottler()
);
});
diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
index 32a507623e3..04de4bc92d3 100644
--- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
+++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php
@@ -27,6 +27,7 @@ namespace OC\AppFramework\Middleware\Security;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Authentication\Exceptions\PasswordLoginForbiddenException;
+use OC\Security\Bruteforce\Throttler;
use OC\User\Session;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
@@ -42,33 +43,29 @@ use OCP\IRequest;
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
*/
class CORSMiddleware extends Middleware {
-
- /**
- * @var IRequest
- */
+ /** @var IRequest */
private $request;
-
- /**
- * @var ControllerMethodReflector
- */
+ /** @var ControllerMethodReflector */
private $reflector;
-
- /**
- * @var Session
- */
+ /** @var Session */
private $session;
+ /** @var Throttler */
+ private $throttler;
/**
* @param IRequest $request
* @param ControllerMethodReflector $reflector
* @param Session $session
+ * @param Throttler $throttler
*/
public function __construct(IRequest $request,
ControllerMethodReflector $reflector,
- Session $session) {
+ Session $session,
+ Throttler $throttler) {
$this->request = $request;
$this->reflector = $reflector;
$this->session = $session;
+ $this->throttler = $throttler;
}
/**
@@ -91,7 +88,7 @@ class CORSMiddleware extends Middleware {
$this->session->logout();
try {
- if (!$this->session->logClientIn($user, $pass, $this->request)) {
+ if (!$this->session->logClientIn($user, $pass, $this->request, $this->throttler)) {
throw new SecurityException('CORS requires basic auth', Http::STATUS_UNAUTHORIZED);
}
} catch (PasswordLoginForbiddenException $ex) {
diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php
new file mode 100644
index 00000000000..5a964dfbaf4
--- /dev/null
+++ b/lib/private/Security/Bruteforce/Throttler.php
@@ -0,0 +1,230 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 OC\Security\Bruteforce;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\ILogger;
+
+/**
+ * Class Throttler implements the bruteforce protection for security actions in
+ * Nextcloud.
+ *
+ * It is working by logging invalid login attempts to the database and slowing
+ * down all login attempts from the same subnet. The max delay is 30 seconds and
+ * the starting delay are 200 milliseconds. (after the first failed login)
+ *
+ * This is based on Paragonie's AirBrake for Airship CMS. You can find the original
+ * code at https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/src/Engine/Security/AirBrake.php
+ *
+ * @package OC\Security\Bruteforce
+ */
+class Throttler {
+ const LOGIN_ACTION = 'login';
+
+ /** @var IDBConnection */
+ private $db;
+ /** @var ITimeFactory */
+ private $timeFactory;
+ /** @var ILogger */
+ private $logger;
+ /** @var IConfig */
+ private $config;
+
+ /**
+ * @param IDBConnection $db
+ * @param ITimeFactory $timeFactory
+ * @param ILogger $logger
+ * @param IConfig $config
+ */
+ public function __construct(IDBConnection $db,
+ ITimeFactory $timeFactory,
+ ILogger $logger,
+ IConfig $config) {
+ $this->db = $db;
+ $this->timeFactory = $timeFactory;
+ $this->logger = $logger;
+ $this->config = $config;
+ }
+
+ /**
+ * Convert a number of seconds into the appropriate DateInterval
+ *
+ * @param int $expire
+ * @return \DateInterval
+ */
+ private function getCutoff($expire) {
+ $d1 = new \DateTime();
+ $d2 = clone $d1;
+ $d2->sub(new \DateInterval('PT' . $expire . 'S'));
+ return $d2->diff($d1);
+ }
+
+ /**
+ * Return the given subnet for an IPv4 address and mask bits
+ *
+ * @param string $ip
+ * @param int $maskBits
+ * @return string
+ */
+ private function getIPv4Subnet($ip,
+ $maskBits = 32) {
+ $binary = \inet_pton($ip);
+ for ($i = 32; $i > $maskBits; $i -= 8) {
+ $j = \intdiv($i, 8) - 1;
+ $k = (int) \min(8, $i - $maskBits);
+ $mask = (0xff - ((pow(2, $k)) - 1));
+ $int = \unpack('C', $binary[$j]);
+ $binary[$j] = \pack('C', $int[1] & $mask);
+ }
+ return \inet_ntop($binary).'/'.$maskBits;
+ }
+
+ /**
+ * Return the given subnet for an IPv6 address and mask bits
+ *
+ * @param string $ip
+ * @param int $maskBits
+ * @return string
+ */
+ private function getIPv6Subnet($ip, $maskBits = 48) {
+ $binary = \inet_pton($ip);
+ for ($i = 128; $i > $maskBits; $i -= 8) {
+ $j = \intdiv($i, 8) - 1;
+ $k = (int) \min(8, $i - $maskBits);
+ $mask = (0xff - ((pow(2, $k)) - 1));
+ $int = \unpack('C', $binary[$j]);
+ $binary[$j] = \pack('C', $int[1] & $mask);
+ }
+ return \inet_ntop($binary).'/'.$maskBits;
+ }
+
+ /**
+ * Return the given subnet for an IP and the configured mask bits
+ *
+ * Determine if the IP is an IPv4 or IPv6 address, then pass to the correct
+ * method for handling that specific type.
+ *
+ * @param string $ip
+ * @return string
+ */
+ private function getSubnet($ip) {
+ if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $ip)) {
+ return $this->getIPv4Subnet(
+ $ip,
+ 32
+ );
+ }
+ return $this->getIPv6Subnet(
+ $ip,
+ 128
+ );
+ }
+
+ /**
+ * Register a failed attempt to bruteforce a security control
+ *
+ * @param string $action
+ * @param string $ip
+ * @param array $metadata Optional metadata logged to the database
+ */
+ public function registerAttempt($action,
+ $ip,
+ array $metadata = []) {
+ // No need to log if the bruteforce protection is disabled
+ if($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
+ return;
+ }
+
+ $values = [
+ 'action' => $action,
+ 'occurred' => $this->timeFactory->getTime(),
+ 'ip' => $ip,
+ 'subnet' => $this->getSubnet($ip),
+ 'metadata' => $metadata,
+ ];
+
+ $this->logger->notice(
+ sprintf(
+ 'Bruteforce attempt from "%s" detected for action "%s".',
+ $ip,
+ $action
+ ),
+ [
+ 'app' => 'core',
+ ]
+ );
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->insert('bruteforce_attempts');
+ foreach($values as $column => $value) {
+ $qb->setValue($column, $qb->createNamedParameter($value));
+ }
+ $qb->execute();
+ }
+
+ /**
+ * Get the throttling delay (in milliseconds)
+ *
+ * @param string $ip
+ * @return int
+ */
+ public function getDelay($ip) {
+ $cutoffTime = (new \DateTime())
+ ->sub($this->getCutoff(43200))
+ ->getTimestamp();
+
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from('bruteforce_attempts')
+ ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
+ ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($this->getSubnet($ip))));
+ $attempts = count($qb->execute()->fetchAll());
+
+ if ($attempts === 0) {
+ return 0;
+ }
+
+ $maxDelay = 30;
+ $firstDelay = 0.1;
+ if ($attempts > (8 * PHP_INT_SIZE - 1)) {
+ // Don't ever overflow. Just assume the maxDelay time:s
+ $firstDelay = $maxDelay;
+ } else {
+ $firstDelay *= pow(2, $attempts);
+ if ($firstDelay > $maxDelay) {
+ $firstDelay = $maxDelay;
+ }
+ }
+ return (int) \ceil($firstDelay * 1000);
+ }
+
+ /**
+ * Will sleep for the defined amount of time
+ *
+ * @param string $ip
+ */
+ public function sleepDelay($ip) {
+ usleep($this->getDelay($ip) * 1000);
+ }
+}
diff --git a/lib/private/Server.php b/lib/private/Server.php
index eb2c26415bc..6ffdeb9211e 100644
--- a/lib/private/Server.php
+++ b/lib/private/Server.php
@@ -66,6 +66,7 @@ use OC\Lock\NoopLockingProvider;
use OC\Mail\Mailer;
use OC\Memcache\ArrayCache;
use OC\Notification\Manager;
+use OC\Security\Bruteforce\Throttler;
use OC\Security\CertificateManager;
use OC\Security\CSP\ContentSecurityPolicyManager;
use OC\Security\Crypto;
@@ -503,6 +504,14 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerService('TrustedDomainHelper', function ($c) {
return new TrustedDomainHelper($this->getConfig());
});
+ $this->registerService('Throttler', function(Server $c) {
+ return new Throttler(
+ $c->getDatabaseConnection(),
+ new TimeFactory(),
+ $c->getLogger(),
+ $c->getConfig()
+ );
+ });
$this->registerService('IntegrityCodeChecker', function (Server $c) {
// IConfig and IAppManager requires a working database. This code
// might however be called when ownCloud is not yet setup.
@@ -1331,6 +1340,13 @@ class Server extends ServerContainer implements IServerContainer {
}
/**
+ * @return Throttler
+ */
+ public function getBruteForceThrottler() {
+ return $this->query('Throttler');
+ }
+
+ /**
* @return IContentSecurityPolicyManager
*/
public function getContentSecurityPolicyManager() {
diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php
index dcc2e66c6c3..79bd7c22848 100644
--- a/lib/private/User/Session.php
+++ b/lib/private/User/Session.php
@@ -95,7 +95,11 @@ class Session implements IUserSession, Emitter {
* @param IProvider $tokenProvider
* @param IConfig $config
*/
- public function __construct(IUserManager $manager, ISession $session, ITimeFactory $timeFacory, $tokenProvider, IConfig $config) {
+ public function __construct(IUserManager $manager,
+ ISession $session,
+ ITimeFactory $timeFacory,
+ $tokenProvider,
+ IConfig $config) {
$this->manager = $manager;
$this->session = $session;
$this->timeFacory = $timeFacory;
@@ -280,7 +284,6 @@ class Session implements IUserSession, Emitter {
*/
public function login($uid, $password) {
$this->session->regenerateId();
-
if ($this->validateToken($password, $uid)) {
return $this->loginWithToken($password);
} else {
@@ -298,11 +301,17 @@ class Session implements IUserSession, Emitter {
* @param string $user
* @param string $password
* @param IRequest $request
+ * @param OC\Security\Bruteforce\Throttler $throttler
* @throws LoginException
* @throws PasswordLoginForbiddenException
* @return boolean
*/
- public function logClientIn($user, $password, IRequest $request) {
+ public function logClientIn($user,
+ $password,
+ IRequest $request,
+ OC\Security\Bruteforce\Throttler $throttler) {
+ $throttler->sleepDelay($request->getRemoteAddress());
+
$isTokenPassword = $this->isTokenPassword($password);
if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
throw new PasswordLoginForbiddenException();
@@ -315,6 +324,8 @@ class Session implements IUserSession, Emitter {
if (count($users) === 1) {
return $this->login($users[0]->getUID(), $password);
}
+
+ $throttler->registerAttempt('login', $request->getRemoteAddress(), ['uid' => $user]);
return false;
}
@@ -391,10 +402,12 @@ class Session implements IUserSession, Emitter {
* @param IRequest $request
* @return boolean if the login was successful
*/
- public function tryBasicAuthLogin(IRequest $request) {
+ public function tryBasicAuthLogin(IRequest $request,
+ OC\Security\Bruteforce\Throttler $throttler) {
if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) {
+ $throttler->sleepDelay(\OC::$server->getRequest()->getRemoteAddress());
try {
- if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request)) {
+ if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) {
/**
* Add DAV authenticated. This should in an ideal world not be
* necessary but the iOS App reads cookies from anywhere instead
diff --git a/lib/private/legacy/api.php b/lib/private/legacy/api.php
index 024f3c0fb63..88eb7b09a78 100644
--- a/lib/private/legacy/api.php
+++ b/lib/private/legacy/api.php
@@ -364,7 +364,7 @@ class OC_API {
try {
$loginSuccess = $userSession->tryTokenLogin($request);
if (!$loginSuccess) {
- $loginSuccess = $userSession->tryBasicAuthLogin($request);
+ $loginSuccess = $userSession->tryBasicAuthLogin($request, \OC::$server->getBruteForceThrottler());
}
} catch (\OC\User\LoginException $e) {
return false;
diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php
index d6fa772d38b..0e13485b272 100644
--- a/tests/Core/Controller/LoginControllerTest.php
+++ b/tests/Core/Controller/LoginControllerTest.php
@@ -23,6 +23,7 @@ namespace Tests\Core\Controller;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Core\Controller\LoginController;
+use OC\Security\Bruteforce\Throttler;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
@@ -51,6 +52,8 @@ class LoginControllerTest extends TestCase {
private $urlGenerator;
/** @var Manager | \PHPUnit_Framework_MockObject_MockObject */
private $twoFactorManager;
+ /** @var Throttler */
+ private $throttler;
public function setUp() {
parent::setUp();
@@ -65,6 +68,9 @@ class LoginControllerTest extends TestCase {
$this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
->disableOriginalConstructor()
->getMock();
+ $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->loginController = new LoginController(
'core',
@@ -74,7 +80,8 @@ class LoginControllerTest extends TestCase {
$this->session,
$this->userSession,
$this->urlGenerator,
- $this->twoFactorManager
+ $this->twoFactorManager,
+ $this->throttler
);
}
@@ -277,10 +284,22 @@ class LoginControllerTest extends TestCase {
}
public function testLoginWithInvalidCredentials() {
- $user = $this->getMock('\OCP\IUser');
+ $user = 'MyUserName';
$password = 'secret';
$loginPageUrl = 'some url';
+ $this->request
+ ->expects($this->exactly(2))
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('registerAttempt')
+ ->with('login', '192.168.0.1', ['user' => 'MyUserName']);
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue(false));
@@ -302,6 +321,14 @@ class LoginControllerTest extends TestCase {
$password = 'secret';
$indexPageUrl = 'some url';
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue($user));
@@ -334,6 +361,14 @@ class LoginControllerTest extends TestCase {
$originalUrl = 'another%20url';
$redirectUrl = 'http://localhost/another url';
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->with('Jane', $password)
@@ -363,6 +398,14 @@ class LoginControllerTest extends TestCase {
$password = 'secret';
$challengeUrl = 'challenge/url';
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue($user));
@@ -412,6 +455,18 @@ class LoginControllerTest extends TestCase {
->method('linkToRoute')
->with('core.login.showLoginForm', ['user' => 'john@doe.com'])
->will($this->returnValue(''));
+ $this->request
+ ->expects($this->exactly(2))
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('registerAttempt')
+ ->with('login', '192.168.0.1', ['user' => 'john@doe.com']);
$expected = new RedirectResponse('');
$this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', 'just wrong', null));
diff --git a/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php b/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php
index 0edf96dd5a4..2e450d897bd 100644
--- a/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php
+++ b/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php
@@ -29,6 +29,9 @@ namespace Test\AppFramework\DependencyInjection;
use \OC\AppFramework\Http\Request;
+/**
+ * @group DB
+ */
class DIContainerTest extends \Test\TestCase {
private $container;
@@ -74,7 +77,6 @@ class DIContainerTest extends \Test\TestCase {
$this->assertEquals('name', $this->container['AppName']);
}
-
public function testMiddlewareDispatcherIncludesSecurityMiddleware(){
$this->container['Request'] = new Request(
['method' => 'GET'],
diff --git a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
index a0dbcc6872a..d0096d43f3d 100644
--- a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
+++ b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
@@ -16,6 +16,7 @@ use OC\AppFramework\Http\Request;
use OC\AppFramework\Middleware\Security\CORSMiddleware;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
+use OC\Security\Bruteforce\Throttler;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
@@ -24,6 +25,8 @@ class CORSMiddlewareTest extends \Test\TestCase {
private $reflector;
private $session;
+ /** @var Throttler */
+ private $throttler;
protected function setUp() {
parent::setUp();
@@ -31,6 +34,9 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->session = $this->getMockBuilder('\OC\User\Session')
->disableOriginalConstructor()
->getMock();
+ $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
+ ->disableOriginalConstructor()
+ ->getMock();
}
/**
@@ -47,7 +53,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();
@@ -65,7 +71,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();
@@ -83,7 +89,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();
@@ -106,7 +112,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = new Response();
$response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
@@ -124,7 +130,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$this->session->expects($this->never())
->method('logout');
$this->session->expects($this->never())
@@ -155,7 +161,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->will($this->returnValue(true));
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$middleware->beforeController($this, __FUNCTION__, new Response());
}
@@ -180,7 +186,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->will($this->throwException(new \OC\Authentication\Exceptions\PasswordLoginForbiddenException));
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$middleware->beforeController($this, __FUNCTION__, new Response());
}
@@ -205,7 +211,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->will($this->returnValue(false));
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$middleware->beforeController($this, __FUNCTION__, new Response());
}
@@ -219,7 +225,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterException($this, __FUNCTION__, new SecurityException('A security exception'));
$expected = new JSONResponse(['message' => 'A security exception'], 500);
@@ -235,7 +241,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterException($this, __FUNCTION__, new SecurityException('A security exception', 501));
$expected = new JSONResponse(['message' => 'A security exception'], 501);
@@ -255,7 +261,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$middleware->afterException($this, __FUNCTION__, new \Exception('A regular exception'));
}
diff --git a/tests/lib/Security/Bruteforce/ThrottlerTest.php b/tests/lib/Security/Bruteforce/ThrottlerTest.php
new file mode 100644
index 00000000000..9b7a47ceec8
--- /dev/null
+++ b/tests/lib/Security/Bruteforce/ThrottlerTest.php
@@ -0,0 +1,123 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @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 Test\Security\Bruteforce;
+
+use OC\AppFramework\Utility\TimeFactory;
+use OC\Security\Bruteforce\Throttler;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\ILogger;
+use Test\TestCase;
+
+/**
+ * Based on the unit tests from Paragonie's Airship CMS
+ * Ref: https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/test/unit/Engine/Security/AirBrakeTest.php
+ *
+ * @package Test\Security\Bruteforce
+ */
+class ThrottlerTest extends TestCase {
+ /** @var Throttler */
+ private $throttler;
+ /** @var IDBConnection */
+ private $dbConnection;
+ /** @var ILogger */
+ private $logger;
+ /** @var IConfig */
+ private $config;
+
+ public function setUp() {
+ $this->dbConnection = $this->getMock('\OCP\IDBConnection');
+ $this->logger = $this->getMock('\OCP\ILogger');
+ $this->config = $this->getMock('\OCP\IConfig');
+
+ $this->throttler = new Throttler(
+ $this->dbConnection,
+ new TimeFactory(),
+ $this->logger,
+ $this->config
+ );
+ return parent::setUp();
+ }
+
+ public function testCutoff() {
+ // precisely 31 second shy of 12 hours
+ $cutoff = $this->invokePrivate($this->throttler, 'getCutoff', [43169]);
+ $this->assertSame(0, $cutoff->y);
+ $this->assertSame(0, $cutoff->m);
+ $this->assertSame(0, $cutoff->d);
+ $this->assertSame(11, $cutoff->h);
+ $this->assertSame(59, $cutoff->i);
+ $this->assertSame(29, $cutoff->s);
+ $cutoff = $this->invokePrivate($this->throttler, 'getCutoff', [86401]);
+ $this->assertSame(0, $cutoff->y);
+ $this->assertSame(0, $cutoff->m);
+ $this->assertSame(1, $cutoff->d);
+ $this->assertSame(0, $cutoff->h);
+ $this->assertSame(0, $cutoff->i);
+ // Leap second tolerance:
+ $this->assertLessThan(2, $cutoff->s);
+ }
+
+ public function testSubnet() {
+ // IPv4
+ $this->assertSame(
+ '64.233.191.254/32',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 32])
+ );
+ $this->assertSame(
+ '64.233.191.252/30',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 30])
+ );
+ $this->assertSame(
+ '64.233.191.240/28',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 28])
+ );
+ $this->assertSame(
+ '64.233.191.0/24',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 24])
+ );
+ $this->assertSame(
+ '64.233.188.0/22',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 22])
+ );
+ // IPv6
+ $this->assertSame(
+ '2001:db8:85a3::8a2e:370:7334/127',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 127])
+ );
+ $this->assertSame(
+ '2001:db8:85a3::8a2e:370:7300/120',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7300', 120])
+ );
+ $this->assertSame(
+ '2001:db8:85a3::/64',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 64])
+ );
+ $this->assertSame(
+ '2001:db8:85a3::/48',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 48])
+ );
+ $this->assertSame(
+ '2001:db8:8500::/40',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 40])
+ );
+ }
+}
diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php
index 9bde2c664b6..33930a50ce5 100644
--- a/tests/lib/User/SessionTest.php
+++ b/tests/lib/User/SessionTest.php
@@ -9,6 +9,7 @@
namespace Test\User;
+use OC\Security\Bruteforce\Throttler;
use OC\Session\Memory;
use OC\User\User;
@@ -17,15 +18,14 @@ use OC\User\User;
* @package Test\User
*/
class SessionTest extends \Test\TestCase {
-
/** @var \OCP\AppFramework\Utility\ITimeFactory */
private $timeFactory;
-
/** @var \OC\Authentication\Token\DefaultTokenProvider */
protected $tokenProvider;
-
/** @var \OCP\IConfig */
private $config;
+ /** @var Throttler */
+ private $throttler;
protected function setUp() {
parent::setUp();
@@ -36,6 +36,8 @@ class SessionTest extends \Test\TestCase {
->will($this->returnValue(10000));
$this->tokenProvider = $this->getMock('\OC\Authentication\Token\IProvider');
$this->config = $this->getMock('\OCP\IConfig');
+ $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
+ ->disableOriginalConstructor()->getMock();
}
public function testGetUser() {
@@ -353,7 +355,6 @@ class SessionTest extends \Test\TestCase {
->getMock();
$session = $this->getMock('\OCP\ISession');
$request = $this->getMock('\OCP\IRequest');
- $user = $this->getMock('\OCP\IUser');
/** @var \OC\User\Session $userSession */
$userSession = $this->getMockBuilder('\OC\User\Session')
@@ -369,8 +370,16 @@ class SessionTest extends \Test\TestCase {
->method('getSystemValue')
->with('token_auth_enforced', false)
->will($this->returnValue(true));
-
- $userSession->logClientIn('john', 'doe', $request);
+ $request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+
+ $userSession->logClientIn('john', 'doe', $request, $this->throttler);
}
public function testLogClientInWithTokenPassword() {
@@ -379,7 +388,6 @@ class SessionTest extends \Test\TestCase {
->getMock();
$session = $this->getMock('\OCP\ISession');
$request = $this->getMock('\OCP\IRequest');
- $user = $this->getMock('\OCP\IUser');
/** @var \OC\User\Session $userSession */
$userSession = $this->getMockBuilder('\OC\User\Session')
@@ -398,8 +406,16 @@ class SessionTest extends \Test\TestCase {
$session->expects($this->once())
->method('set')
->with('app_password', 'I-AM-AN-APP-PASSWORD');
-
- $this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request));
+ $request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+
+ $this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request, $this->throttler));
}
/**
@@ -410,7 +426,6 @@ class SessionTest extends \Test\TestCase {
->disableOriginalConstructor()
->getMock();
$session = $this->getMock('\OCP\ISession');
- $user = $this->getMock('\OCP\IUser');
$request = $this->getMock('\OCP\IRequest');
/** @var \OC\User\Session $userSession */
@@ -433,7 +448,16 @@ class SessionTest extends \Test\TestCase {
->with('john')
->will($this->returnValue(true));
- $userSession->logClientIn('john', 'doe', $request);
+ $request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+
+ $userSession->logClientIn('john', 'doe', $request, $this->throttler);
}
public function testRememberLoginValidToken() {
diff --git a/version.php b/version.php
index 15400c4f4e5..5f88d29b155 100644
--- a/version.php
+++ b/version.php
@@ -25,7 +25,7 @@
// We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel
// when updating major/minor version number.
-$OC_Version = array(9, 1, 0, 12);
+$OC_Version = array(9, 1, 0, 13);
// The human readable string
$OC_VersionString = '9.1.0 RC1';