Merge pull request #479 from nextcloud/add-bruteforce-throttler
Implement brute force protection
This commit is contained in:
commit
c385423d10
|
@ -35,6 +35,7 @@ $authBackend = new Auth(
|
|||
\OC::$server->getUserSession(),
|
||||
\OC::$server->getRequest(),
|
||||
\OC::$server->getTwoFactorAuthManager(),
|
||||
\OC::$server->getBruteForceThrottler(),
|
||||
'principals/'
|
||||
);
|
||||
$principalBackend = new Principal(
|
||||
|
|
|
@ -36,6 +36,7 @@ $authBackend = new Auth(
|
|||
\OC::$server->getUserSession(),
|
||||
\OC::$server->getRequest(),
|
||||
\OC::$server->getTwoFactorAuthManager(),
|
||||
\OC::$server->getBruteForceThrottler(),
|
||||
'principals/'
|
||||
);
|
||||
$principalBackend = new Principal(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -207,6 +207,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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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']
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1163,6 +1163,84 @@
|
|||
</declaration>
|
||||
</table>
|
||||
|
||||
<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>
|
||||
|
||||
<!--
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
@ -1330,6 +1339,13 @@ class Server extends ServerContainer implements IServerContainer {
|
|||
return $this->query('CsrfTokenManager');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Throttler
|
||||
*/
|
||||
public function getBruteForceThrottler() {
|
||||
return $this->query('Throttler');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IContentSecurityPolicyManager
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
|
|
|
@ -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])
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
$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);
|
||||
$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');
|
||||
$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->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() {
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in New Issue