Parcourir la source

Implement brute force protection

Class Throttler implements the bruteforce protection for security actions in
Nextcloud.

It is working by logging invalid login attempts to the database and slowing
down all login attempts from the same subnet. The max delay is 30 seconds and
the starting delay are 200 milliseconds. (after the first failed login)
tags/v10.0RC1
Lukas Reschke il y a 7 ans
Parent
révision
ba4f12baa0
Aucun compte lié à l'adresse e-mail de l'auteur

+ 1
- 0
apps/dav/appinfo/v1/caldav.php Voir le fichier

@@ -35,6 +35,7 @@ $authBackend = new Auth(
\OC::$server->getUserSession(),
\OC::$server->getRequest(),
\OC::$server->getTwoFactorAuthManager(),
\OC::$server->getBruteForceThrottler(),
'principals/'
);
$principalBackend = new Principal(

+ 1
- 0
apps/dav/appinfo/v1/carddav.php Voir le fichier

@@ -36,6 +36,7 @@ $authBackend = new Auth(
\OC::$server->getUserSession(),
\OC::$server->getRequest(),
\OC::$server->getTwoFactorAuthManager(),
\OC::$server->getBruteForceThrottler(),
'principals/'
);
$principalBackend = new Principal(

+ 1
- 0
apps/dav/appinfo/v1/webdav.php Voir le fichier

@@ -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();

+ 8
- 1
apps/dav/lib/Connector/Sabre/Auth.php Voir le fichier

@@ -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();

+ 2
- 1
apps/dav/lib/Server.php Voir le fichier

@@ -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

+ 8
- 1
apps/dav/tests/unit/Connector/Sabre/AuthTest.php Voir le fichier

@@ -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
);
}


+ 3
- 0
build/integration/run.sh Voir le fichier

@@ -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

+ 7
- 0
config/config.sample.php Voir le fichier

@@ -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

+ 2
- 1
core/Application.php Voir le fichier

@@ -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) {

+ 19
- 8
core/Controller/LoginController.php Voir le fichier

@@ -22,7 +22,9 @@

namespace OC\Core\Controller;

use OC\AppFramework\Utility\TimeFactory;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Security\Bruteforce\Throttler;
use OC\User\Session;
use OC_App;
use OC_Util;
@@ -37,24 +39,20 @@ use OCP\IUser;
use OCP\IUserManager;

class LoginController extends Controller {

/** @var IUserManager */
private $userManager;

/** @var IConfig */
private $config;

/** @var ISession */
private $session;

/** @var Session */
private $userSession;

/** @var IURLGenerator */
private $urlGenerator;

/** @var Manager */
private $twoFactorManager;
/** @var Throttler */
private $throttler;

/**
* @param string $appName
@@ -65,9 +63,17 @@ class LoginController extends Controller {
* @param Session $userSession
* @param IURLGenerator $urlGenerator
* @param Manager $twoFactorManager
* @param Throttler $throttler
*/
function __construct($appName, IRequest $request, IUserManager $userManager, IConfig $config, ISession $session,
Session $userSession, IURLGenerator $urlGenerator, Manager $twoFactorManager) {
function __construct($appName,
IRequest $request,
IUserManager $userManager,
IConfig $config,
ISession $session,
Session $userSession,
IURLGenerator $urlGenerator,
Manager $twoFactorManager,
Throttler $throttler) {
parent::__construct($appName, $request);
$this->userManager = $userManager;
$this->config = $config;
@@ -75,6 +81,7 @@ class LoginController extends Controller {
$this->userSession = $userSession;
$this->urlGenerator = $urlGenerator;
$this->twoFactorManager = $twoFactorManager;
$this->throttler = $throttler;
}

/**
@@ -171,6 +178,8 @@ class LoginController extends Controller {
* @return RedirectResponse
*/
public function tryLogin($user, $password, $redirect_url) {
$this->throttler->sleepDelay($this->request->getRemoteAddress());

$originalUser = $user;
// TODO: Add all the insane error handling
/* @var $loginResult IUser */
@@ -184,6 +193,8 @@ class LoginController extends Controller {
}
}
if ($loginResult === false) {
$this->throttler->registerAttempt('login', $this->request->getRemoteAddress(), ['user' => $originalUser]);

$this->session->set('loginMessages', [
['invalidpassword']
]);

+ 10
- 8
core/Controller/TokenController.php Voir le fichier

@@ -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;

+ 78
- 0
db_structure.xml Voir le fichier

@@ -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>

<!--

+ 1
- 1
lib/base.php Voir le fichier

@@ -913,7 +913,7 @@ class OC {
if ($userSession->tryTokenLogin($request)) {
return true;
}
if ($userSession->tryBasicAuthLogin($request)) {
if ($userSession->tryBasicAuthLogin($request, \OC::$server->getBruteForceThrottler())) {
return true;
}
return false;

+ 2
- 1
lib/private/AppFramework/DependencyInjection/DIContainer.php Voir le fichier

@@ -351,7 +351,8 @@ class DIContainer extends SimpleContainer implements IAppContainer {
return new CORSMiddleware(
$c['Request'],
$c['ControllerMethodReflector'],
$c['OCP\IUserSession']
$c['OCP\IUserSession'],
$c->getServer()->getBruteForceThrottler()
);
});


+ 11
- 14
lib/private/AppFramework/Middleware/Security/CORSMiddleware.php Voir le fichier

@@ -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) {

+ 230
- 0
lib/private/Security/Bruteforce/Throttler.php Voir le fichier

@@ -0,0 +1,230 @@
<?php
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OC\Security\Bruteforce;

use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\ILogger;

/**
* Class Throttler implements the bruteforce protection for security actions in
* Nextcloud.
*
* It is working by logging invalid login attempts to the database and slowing
* down all login attempts from the same subnet. The max delay is 30 seconds and
* the starting delay are 200 milliseconds. (after the first failed login)
*
* This is based on Paragonie's AirBrake for Airship CMS. You can find the original
* code at https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/src/Engine/Security/AirBrake.php
*
* @package OC\Security\Bruteforce
*/
class Throttler {
const LOGIN_ACTION = 'login';

/** @var IDBConnection */
private $db;
/** @var ITimeFactory */
private $timeFactory;
/** @var ILogger */
private $logger;
/** @var IConfig */
private $config;

/**
* @param IDBConnection $db
* @param ITimeFactory $timeFactory
* @param ILogger $logger
* @param IConfig $config
*/
public function __construct(IDBConnection $db,
ITimeFactory $timeFactory,
ILogger $logger,
IConfig $config) {
$this->db = $db;
$this->timeFactory = $timeFactory;
$this->logger = $logger;
$this->config = $config;
}

/**
* Convert a number of seconds into the appropriate DateInterval
*
* @param int $expire
* @return \DateInterval
*/
private function getCutoff($expire) {
$d1 = new \DateTime();
$d2 = clone $d1;
$d2->sub(new \DateInterval('PT' . $expire . 'S'));
return $d2->diff($d1);
}

/**
* Return the given subnet for an IPv4 address and mask bits
*
* @param string $ip
* @param int $maskBits
* @return string
*/
private function getIPv4Subnet($ip,
$maskBits = 32) {
$binary = \inet_pton($ip);
for ($i = 32; $i > $maskBits; $i -= 8) {
$j = \intdiv($i, 8) - 1;
$k = (int) \min(8, $i - $maskBits);
$mask = (0xff - ((pow(2, $k)) - 1));
$int = \unpack('C', $binary[$j]);
$binary[$j] = \pack('C', $int[1] & $mask);
}
return \inet_ntop($binary).'/'.$maskBits;
}

/**
* Return the given subnet for an IPv6 address and mask bits
*
* @param string $ip
* @param int $maskBits
* @return string
*/
private function getIPv6Subnet($ip, $maskBits = 48) {
$binary = \inet_pton($ip);
for ($i = 128; $i > $maskBits; $i -= 8) {
$j = \intdiv($i, 8) - 1;
$k = (int) \min(8, $i - $maskBits);
$mask = (0xff - ((pow(2, $k)) - 1));
$int = \unpack('C', $binary[$j]);
$binary[$j] = \pack('C', $int[1] & $mask);
}
return \inet_ntop($binary).'/'.$maskBits;
}

/**
* Return the given subnet for an IP and the configured mask bits
*
* Determine if the IP is an IPv4 or IPv6 address, then pass to the correct
* method for handling that specific type.
*
* @param string $ip
* @return string
*/
private function getSubnet($ip) {
if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $ip)) {
return $this->getIPv4Subnet(
$ip,
32
);
}
return $this->getIPv6Subnet(
$ip,
128
);
}

/**
* Register a failed attempt to bruteforce a security control
*
* @param string $action
* @param string $ip
* @param array $metadata Optional metadata logged to the database
*/
public function registerAttempt($action,
$ip,
array $metadata = []) {
// No need to log if the bruteforce protection is disabled
if($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) {
return;
}

$values = [
'action' => $action,
'occurred' => $this->timeFactory->getTime(),
'ip' => $ip,
'subnet' => $this->getSubnet($ip),
'metadata' => $metadata,
];

$this->logger->notice(
sprintf(
'Bruteforce attempt from "%s" detected for action "%s".',
$ip,
$action
),
[
'app' => 'core',
]
);

$qb = $this->db->getQueryBuilder();
$qb->insert('bruteforce_attempts');
foreach($values as $column => $value) {
$qb->setValue($column, $qb->createNamedParameter($value));
}
$qb->execute();
}

/**
* Get the throttling delay (in milliseconds)
*
* @param string $ip
* @return int
*/
public function getDelay($ip) {
$cutoffTime = (new \DateTime())
->sub($this->getCutoff(43200))
->getTimestamp();

$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('bruteforce_attempts')
->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime)))
->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($this->getSubnet($ip))));
$attempts = count($qb->execute()->fetchAll());

if ($attempts === 0) {
return 0;
}

$maxDelay = 30;
$firstDelay = 0.1;
if ($attempts > (8 * PHP_INT_SIZE - 1)) {
// Don't ever overflow. Just assume the maxDelay time:s
$firstDelay = $maxDelay;
} else {
$firstDelay *= pow(2, $attempts);
if ($firstDelay > $maxDelay) {
$firstDelay = $maxDelay;
}
}
return (int) \ceil($firstDelay * 1000);
}

/**
* Will sleep for the defined amount of time
*
* @param string $ip
*/
public function sleepDelay($ip) {
usleep($this->getDelay($ip) * 1000);
}
}

+ 16
- 0
lib/private/Server.php Voir le fichier

@@ -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
*/

+ 18
- 5
lib/private/User/Session.php Voir le fichier

@@ -95,7 +95,11 @@ class Session implements IUserSession, Emitter {
* @param IProvider $tokenProvider
* @param IConfig $config
*/
public function __construct(IUserManager $manager, ISession $session, ITimeFactory $timeFacory, $tokenProvider, IConfig $config) {
public function __construct(IUserManager $manager,
ISession $session,
ITimeFactory $timeFacory,
$tokenProvider,
IConfig $config) {
$this->manager = $manager;
$this->session = $session;
$this->timeFacory = $timeFacory;
@@ -280,7 +284,6 @@ class Session implements IUserSession, Emitter {
*/
public function login($uid, $password) {
$this->session->regenerateId();

if ($this->validateToken($password, $uid)) {
return $this->loginWithToken($password);
} else {
@@ -298,11 +301,17 @@ class Session implements IUserSession, Emitter {
* @param string $user
* @param string $password
* @param IRequest $request
* @param OC\Security\Bruteforce\Throttler $throttler
* @throws LoginException
* @throws PasswordLoginForbiddenException
* @return boolean
*/
public function logClientIn($user, $password, IRequest $request) {
public function logClientIn($user,
$password,
IRequest $request,
OC\Security\Bruteforce\Throttler $throttler) {
$throttler->sleepDelay($request->getRemoteAddress());

$isTokenPassword = $this->isTokenPassword($password);
if (!$isTokenPassword && $this->isTokenAuthEnforced()) {
throw new PasswordLoginForbiddenException();
@@ -315,6 +324,8 @@ class Session implements IUserSession, Emitter {
if (count($users) === 1) {
return $this->login($users[0]->getUID(), $password);
}

$throttler->registerAttempt('login', $request->getRemoteAddress(), ['uid' => $user]);
return false;
}

@@ -391,10 +402,12 @@ class Session implements IUserSession, Emitter {
* @param IRequest $request
* @return boolean if the login was successful
*/
public function tryBasicAuthLogin(IRequest $request) {
public function tryBasicAuthLogin(IRequest $request,
OC\Security\Bruteforce\Throttler $throttler) {
if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) {
$throttler->sleepDelay(\OC::$server->getRequest()->getRemoteAddress());
try {
if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request)) {
if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) {
/**
* Add DAV authenticated. This should in an ideal world not be
* necessary but the iOS App reads cookies from anywhere instead

+ 1
- 1
lib/private/legacy/api.php Voir le fichier

@@ -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;

+ 57
- 2
tests/Core/Controller/LoginControllerTest.php Voir le fichier

@@ -23,6 +23,7 @@ namespace Tests\Core\Controller;

use OC\Authentication\TwoFactorAuth\Manager;
use OC\Core\Controller\LoginController;
use OC\Security\Bruteforce\Throttler;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
@@ -51,6 +52,8 @@ class LoginControllerTest extends TestCase {
private $urlGenerator;
/** @var Manager | \PHPUnit_Framework_MockObject_MockObject */
private $twoFactorManager;
/** @var Throttler */
private $throttler;

public function setUp() {
parent::setUp();
@@ -65,6 +68,9 @@ class LoginControllerTest extends TestCase {
$this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
->disableOriginalConstructor()
->getMock();
$this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
->disableOriginalConstructor()
->getMock();

$this->loginController = new LoginController(
'core',
@@ -74,7 +80,8 @@ class LoginControllerTest extends TestCase {
$this->session,
$this->userSession,
$this->urlGenerator,
$this->twoFactorManager
$this->twoFactorManager,
$this->throttler
);
}

@@ -277,10 +284,22 @@ class LoginControllerTest extends TestCase {
}

public function testLoginWithInvalidCredentials() {
$user = $this->getMock('\OCP\IUser');
$user = 'MyUserName';
$password = 'secret';
$loginPageUrl = 'some url';

$this->request
->expects($this->exactly(2))
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelay')
->with('192.168.0.1');
$this->throttler
->expects($this->once())
->method('registerAttempt')
->with('login', '192.168.0.1', ['user' => 'MyUserName']);
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue(false));
@@ -302,6 +321,14 @@ class LoginControllerTest extends TestCase {
$password = 'secret';
$indexPageUrl = 'some url';

$this->request
->expects($this->once())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelay')
->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue($user));
@@ -334,6 +361,14 @@ class LoginControllerTest extends TestCase {
$originalUrl = 'another%20url';
$redirectUrl = 'http://localhost/another url';

$this->request
->expects($this->once())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelay')
->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->with('Jane', $password)
@@ -363,6 +398,14 @@ class LoginControllerTest extends TestCase {
$password = 'secret';
$challengeUrl = 'challenge/url';

$this->request
->expects($this->once())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelay')
->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue($user));
@@ -412,6 +455,18 @@ class LoginControllerTest extends TestCase {
->method('linkToRoute')
->with('core.login.showLoginForm', ['user' => 'john@doe.com'])
->will($this->returnValue(''));
$this->request
->expects($this->exactly(2))
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelay')
->with('192.168.0.1');
$this->throttler
->expects($this->once())
->method('registerAttempt')
->with('login', '192.168.0.1', ['user' => 'john@doe.com']);

$expected = new RedirectResponse('');
$this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', 'just wrong', null));

+ 3
- 1
tests/lib/AppFramework/DependencyInjection/DIContainerTest.php Voir le fichier

@@ -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'],

+ 17
- 11
tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php Voir le fichier

@@ -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'));
}


+ 123
- 0
tests/lib/Security/Bruteforce/ThrottlerTest.php Voir le fichier

@@ -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])
);
}
}

+ 35
- 11
tests/lib/User/SessionTest.php Voir le fichier

@@ -9,6 +9,7 @@

namespace Test\User;

use OC\Security\Bruteforce\Throttler;
use OC\Session\Memory;
use OC\User\User;

@@ -17,15 +18,14 @@ use OC\User\User;
* @package Test\User
*/
class SessionTest extends \Test\TestCase {

/** @var \OCP\AppFramework\Utility\ITimeFactory */
private $timeFactory;

/** @var \OC\Authentication\Token\DefaultTokenProvider */
protected $tokenProvider;

/** @var \OCP\IConfig */
private $config;
/** @var Throttler */
private $throttler;

protected function setUp() {
parent::setUp();
@@ -36,6 +36,8 @@ class SessionTest extends \Test\TestCase {
->will($this->returnValue(10000));
$this->tokenProvider = $this->getMock('\OC\Authentication\Token\IProvider');
$this->config = $this->getMock('\OCP\IConfig');
$this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
->disableOriginalConstructor()->getMock();
}

public function testGetUser() {
@@ -353,7 +355,6 @@ class SessionTest extends \Test\TestCase {
->getMock();
$session = $this->getMock('\OCP\ISession');
$request = $this->getMock('\OCP\IRequest');
$user = $this->getMock('\OCP\IUser');

/** @var \OC\User\Session $userSession */
$userSession = $this->getMockBuilder('\OC\User\Session')
@@ -369,8 +370,16 @@ class SessionTest extends \Test\TestCase {
->method('getSystemValue')
->with('token_auth_enforced', false)
->will($this->returnValue(true));

$userSession->logClientIn('john', 'doe', $request);
$request
->expects($this->once())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelay')
->with('192.168.0.1');

$userSession->logClientIn('john', 'doe', $request, $this->throttler);
}

public function testLogClientInWithTokenPassword() {
@@ -379,7 +388,6 @@ class SessionTest extends \Test\TestCase {
->getMock();
$session = $this->getMock('\OCP\ISession');
$request = $this->getMock('\OCP\IRequest');
$user = $this->getMock('\OCP\IUser');

/** @var \OC\User\Session $userSession */
$userSession = $this->getMockBuilder('\OC\User\Session')
@@ -398,8 +406,16 @@ class SessionTest extends \Test\TestCase {
$session->expects($this->once())
->method('set')
->with('app_password', 'I-AM-AN-APP-PASSWORD');

$this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request));
$request
->expects($this->once())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelay')
->with('192.168.0.1');

$this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request, $this->throttler));
}

/**
@@ -410,7 +426,6 @@ class SessionTest extends \Test\TestCase {
->disableOriginalConstructor()
->getMock();
$session = $this->getMock('\OCP\ISession');
$user = $this->getMock('\OCP\IUser');
$request = $this->getMock('\OCP\IRequest');

/** @var \OC\User\Session $userSession */
@@ -433,7 +448,16 @@ class SessionTest extends \Test\TestCase {
->with('john')
->will($this->returnValue(true));

$userSession->logClientIn('john', 'doe', $request);
$request
->expects($this->once())
->method('getRemoteAddress')
->willReturn('192.168.0.1');
$this->throttler
->expects($this->once())
->method('sleepDelay')
->with('192.168.0.1');

$userSession->logClientIn('john', 'doe', $request, $this->throttler);
}

public function testRememberLoginValidToken() {

+ 1
- 1
version.php Voir le fichier

@@ -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';

Chargement…
Annuler
Enregistrer