summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLukas Reschke <lukas@statuscode.ch>2016-07-21 00:31:02 +0200
committerGitHub <noreply@github.com>2016-07-21 00:31:02 +0200
commitc385423d1096c243050fed3585734c308115864b (patch)
tree1002bfc475cd88a7cc495f4ffc23bbd03ec75d39
parent020a2a6958e48f7a3a29daa2235f6729980850af (diff)
parentc1589f163c44839fba9b2d3dcfb1e45ee7fa47ef (diff)
downloadnextcloud-server-c385423d1096c243050fed3585734c308115864b.tar.gz
nextcloud-server-c385423d1096c243050fed3585734c308115864b.zip
Merge pull request #479 from nextcloud/add-bruteforce-throttler
Implement brute force protection
-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.php30
-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.php26
-rw-r--r--lib/private/legacy/api.php2
-rw-r--r--tests/Core/Controller/LoginControllerTest.php84
-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.php61
-rw-r--r--version.php2
25 files changed, 701 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 73a07072d3c..982d299c951 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..66bb13dbb54 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,9 @@ class LoginController extends Controller {
* @return RedirectResponse
*/
public function tryLogin($user, $password, $redirect_url) {
+ $currentDelay = $this->throttler->getDelay($this->request->getRemoteAddress());
+ $this->throttler->sleepDelay($this->request->getRemoteAddress());
+
$originalUser = $user;
// TODO: Add all the insane error handling
/* @var $loginResult IUser */
@@ -184,6 +194,10 @@ class LoginController extends Controller {
}
}
if ($loginResult === false) {
+ $this->throttler->registerAttempt('login', $this->request->getRemoteAddress(), ['user' => $originalUser]);
+ if($currentDelay === 0) {
+ $this->throttler->sleepDelay($this->request->getRemoteAddress());
+ }
$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 afa13cd1047..b2ac0bab7d8 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -993,7 +993,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 32a85606abf..893d6cb9aa6 100644
--- a/lib/private/AppFramework/DependencyInjection/DIContainer.php
+++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php
@@ -352,7 +352,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..0de7677285b
--- /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' => json_encode($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..8d12982dd1a 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,18 @@ 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) {
+ $currentDelay = $throttler->getDelay($request->getRemoteAddress());
+ $throttler->sleepDelay($request->getRemoteAddress());
+
$isTokenPassword = $this->isTokenPassword($password);
if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
throw new PasswordLoginForbiddenException();
@@ -315,6 +325,11 @@ class Session implements IUserSession, Emitter {
if (count($users) === 1) {
return $this->login($users[0]->getUID(), $password);
}
+
+ $throttler->registerAttempt('login', $request->getRemoteAddress(), ['uid' => $user]);
+ if($currentDelay === 0) {
+ $throttler->sleepDelay($request->getRemoteAddress());
+ }
return false;
}
@@ -391,10 +406,11 @@ 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'])) {
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..f09f3c98118 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,27 @@ class LoginControllerTest extends TestCase {
}
public function testLoginWithInvalidCredentials() {
- $user = $this->getMock('\OCP\IUser');
+ $user = 'MyUserName';
$password = 'secret';
$loginPageUrl = 'some url';
+ $this->request
+ ->expects($this->exactly(4))
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->exactly(2))
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(0);
+ $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 +326,19 @@ class LoginControllerTest extends TestCase {
$password = 'secret';
$indexPageUrl = '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('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(200);
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue($user));
@@ -334,6 +371,19 @@ class LoginControllerTest extends TestCase {
$originalUrl = 'another%20url';
$redirectUrl = 'http://localhost/another 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('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(200);
$this->userManager->expects($this->once())
->method('checkPassword')
->with('Jane', $password)
@@ -363,6 +413,19 @@ class LoginControllerTest extends TestCase {
$password = 'secret';
$challengeUrl = 'challenge/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('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(200);
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue($user));
@@ -412,6 +475,23 @@ class LoginControllerTest extends TestCase {
->method('linkToRoute')
->with('core.login.showLoginForm', ['user' => 'john@doe.com'])
->will($this->returnValue(''));
+ $this->request
+ ->expects($this->exactly(3))
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(200);
+ $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..379c7e39442 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,21 @@ class SessionTest extends \Test\TestCase {
->method('getSystemValue')
->with('token_auth_enforced', false)
->will($this->returnValue(true));
-
- $userSession->logClientIn('john', 'doe', $request);
+ $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('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(0);
+
+ $userSession->logClientIn('john', 'doe', $request, $this->throttler);
}
public function testLogClientInWithTokenPassword() {
@@ -379,7 +393,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 +411,21 @@ 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->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('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(0);
+
+ $this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request, $this->throttler));
}
/**
@@ -410,7 +436,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 +458,21 @@ class SessionTest extends \Test\TestCase {
->with('john')
->will($this->returnValue(true));
- $userSession->logClientIn('john', 'doe', $request);
+ $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('getDelay')
+ ->with('192.168.0.1')
+ ->willReturn(0);
+
+ $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';