summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorLukas Reschke <lukas@statuscode.ch>2016-07-20 18:36:15 +0200
committerLukas Reschke <lukas@statuscode.ch>2016-07-20 22:08:56 +0200
commitba4f12baa02dfb55ec8822687896d643261440c4 (patch)
tree5dc95ab54a2ae169951693a43ba7aa6920d6f36a /tests
parent7cdf6402ff9a0e07866ca8bcfcffd0e0897b646a (diff)
downloadnextcloud-server-ba4f12baa02dfb55ec8822687896d643261440c4.tar.gz
nextcloud-server-ba4f12baa02dfb55ec8822687896d643261440c4.zip
Implement brute force protection
Class Throttler implements the bruteforce protection for security actions in Nextcloud. It is working by logging invalid login attempts to the database and slowing down all login attempts from the same subnet. The max delay is 30 seconds and the starting delay are 200 milliseconds. (after the first failed login)
Diffstat (limited to 'tests')
-rw-r--r--tests/Core/Controller/LoginControllerTest.php59
-rw-r--r--tests/lib/AppFramework/DependencyInjection/DIContainerTest.php4
-rw-r--r--tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php28
-rw-r--r--tests/lib/Security/Bruteforce/ThrottlerTest.php123
-rw-r--r--tests/lib/User/SessionTest.php46
5 files changed, 235 insertions, 25 deletions
diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php
index d6fa772d38b..0e13485b272 100644
--- a/tests/Core/Controller/LoginControllerTest.php
+++ b/tests/Core/Controller/LoginControllerTest.php
@@ -23,6 +23,7 @@ namespace Tests\Core\Controller;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Core\Controller\LoginController;
+use OC\Security\Bruteforce\Throttler;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
@@ -51,6 +52,8 @@ class LoginControllerTest extends TestCase {
private $urlGenerator;
/** @var Manager | \PHPUnit_Framework_MockObject_MockObject */
private $twoFactorManager;
+ /** @var Throttler */
+ private $throttler;
public function setUp() {
parent::setUp();
@@ -65,6 +68,9 @@ class LoginControllerTest extends TestCase {
$this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager')
->disableOriginalConstructor()
->getMock();
+ $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->loginController = new LoginController(
'core',
@@ -74,7 +80,8 @@ class LoginControllerTest extends TestCase {
$this->session,
$this->userSession,
$this->urlGenerator,
- $this->twoFactorManager
+ $this->twoFactorManager,
+ $this->throttler
);
}
@@ -277,10 +284,22 @@ class LoginControllerTest extends TestCase {
}
public function testLoginWithInvalidCredentials() {
- $user = $this->getMock('\OCP\IUser');
+ $user = 'MyUserName';
$password = 'secret';
$loginPageUrl = 'some url';
+ $this->request
+ ->expects($this->exactly(2))
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('registerAttempt')
+ ->with('login', '192.168.0.1', ['user' => 'MyUserName']);
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue(false));
@@ -302,6 +321,14 @@ class LoginControllerTest extends TestCase {
$password = 'secret';
$indexPageUrl = 'some url';
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue($user));
@@ -334,6 +361,14 @@ class LoginControllerTest extends TestCase {
$originalUrl = 'another%20url';
$redirectUrl = 'http://localhost/another url';
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->with('Jane', $password)
@@ -363,6 +398,14 @@ class LoginControllerTest extends TestCase {
$password = 'secret';
$challengeUrl = 'challenge/url';
+ $this->request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
$this->userManager->expects($this->once())
->method('checkPassword')
->will($this->returnValue($user));
@@ -412,6 +455,18 @@ class LoginControllerTest extends TestCase {
->method('linkToRoute')
->with('core.login.showLoginForm', ['user' => 'john@doe.com'])
->will($this->returnValue(''));
+ $this->request
+ ->expects($this->exactly(2))
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('registerAttempt')
+ ->with('login', '192.168.0.1', ['user' => 'john@doe.com']);
$expected = new RedirectResponse('');
$this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', 'just wrong', null));
diff --git a/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php b/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php
index 0edf96dd5a4..2e450d897bd 100644
--- a/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php
+++ b/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php
@@ -29,6 +29,9 @@ namespace Test\AppFramework\DependencyInjection;
use \OC\AppFramework\Http\Request;
+/**
+ * @group DB
+ */
class DIContainerTest extends \Test\TestCase {
private $container;
@@ -74,7 +77,6 @@ class DIContainerTest extends \Test\TestCase {
$this->assertEquals('name', $this->container['AppName']);
}
-
public function testMiddlewareDispatcherIncludesSecurityMiddleware(){
$this->container['Request'] = new Request(
['method' => 'GET'],
diff --git a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
index a0dbcc6872a..d0096d43f3d 100644
--- a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
+++ b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php
@@ -16,6 +16,7 @@ use OC\AppFramework\Http\Request;
use OC\AppFramework\Middleware\Security\CORSMiddleware;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
+use OC\Security\Bruteforce\Throttler;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
@@ -24,6 +25,8 @@ class CORSMiddlewareTest extends \Test\TestCase {
private $reflector;
private $session;
+ /** @var Throttler */
+ private $throttler;
protected function setUp() {
parent::setUp();
@@ -31,6 +34,9 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->session = $this->getMockBuilder('\OC\User\Session')
->disableOriginalConstructor()
->getMock();
+ $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
+ ->disableOriginalConstructor()
+ ->getMock();
}
/**
@@ -47,7 +53,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();
@@ -65,7 +71,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();
@@ -83,7 +89,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterController($this, __FUNCTION__, new Response());
$headers = $response->getHeaders();
@@ -106,7 +112,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = new Response();
$response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE');
@@ -124,7 +130,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$this->session->expects($this->never())
->method('logout');
$this->session->expects($this->never())
@@ -155,7 +161,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->will($this->returnValue(true));
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$middleware->beforeController($this, __FUNCTION__, new Response());
}
@@ -180,7 +186,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->will($this->throwException(new \OC\Authentication\Exceptions\PasswordLoginForbiddenException));
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$middleware->beforeController($this, __FUNCTION__, new Response());
}
@@ -205,7 +211,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
->with($this->equalTo('user'), $this->equalTo('pass'))
->will($this->returnValue(false));
$this->reflector->reflect($this, __FUNCTION__);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$middleware->beforeController($this, __FUNCTION__, new Response());
}
@@ -219,7 +225,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterException($this, __FUNCTION__, new SecurityException('A security exception'));
$expected = new JSONResponse(['message' => 'A security exception'], 500);
@@ -235,7 +241,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$response = $middleware->afterException($this, __FUNCTION__, new SecurityException('A security exception', 501));
$expected = new JSONResponse(['message' => 'A security exception'], 501);
@@ -255,7 +261,7 @@ class CORSMiddlewareTest extends \Test\TestCase {
$this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(),
$this->getMockBuilder('\OCP\IConfig')->getMock()
);
- $middleware = new CORSMiddleware($request, $this->reflector, $this->session);
+ $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler);
$middleware->afterException($this, __FUNCTION__, new \Exception('A regular exception'));
}
diff --git a/tests/lib/Security/Bruteforce/ThrottlerTest.php b/tests/lib/Security/Bruteforce/ThrottlerTest.php
new file mode 100644
index 00000000000..9b7a47ceec8
--- /dev/null
+++ b/tests/lib/Security/Bruteforce/ThrottlerTest.php
@@ -0,0 +1,123 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace Test\Security\Bruteforce;
+
+use OC\AppFramework\Utility\TimeFactory;
+use OC\Security\Bruteforce\Throttler;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\ILogger;
+use Test\TestCase;
+
+/**
+ * Based on the unit tests from Paragonie's Airship CMS
+ * Ref: https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/test/unit/Engine/Security/AirBrakeTest.php
+ *
+ * @package Test\Security\Bruteforce
+ */
+class ThrottlerTest extends TestCase {
+ /** @var Throttler */
+ private $throttler;
+ /** @var IDBConnection */
+ private $dbConnection;
+ /** @var ILogger */
+ private $logger;
+ /** @var IConfig */
+ private $config;
+
+ public function setUp() {
+ $this->dbConnection = $this->getMock('\OCP\IDBConnection');
+ $this->logger = $this->getMock('\OCP\ILogger');
+ $this->config = $this->getMock('\OCP\IConfig');
+
+ $this->throttler = new Throttler(
+ $this->dbConnection,
+ new TimeFactory(),
+ $this->logger,
+ $this->config
+ );
+ return parent::setUp();
+ }
+
+ public function testCutoff() {
+ // precisely 31 second shy of 12 hours
+ $cutoff = $this->invokePrivate($this->throttler, 'getCutoff', [43169]);
+ $this->assertSame(0, $cutoff->y);
+ $this->assertSame(0, $cutoff->m);
+ $this->assertSame(0, $cutoff->d);
+ $this->assertSame(11, $cutoff->h);
+ $this->assertSame(59, $cutoff->i);
+ $this->assertSame(29, $cutoff->s);
+ $cutoff = $this->invokePrivate($this->throttler, 'getCutoff', [86401]);
+ $this->assertSame(0, $cutoff->y);
+ $this->assertSame(0, $cutoff->m);
+ $this->assertSame(1, $cutoff->d);
+ $this->assertSame(0, $cutoff->h);
+ $this->assertSame(0, $cutoff->i);
+ // Leap second tolerance:
+ $this->assertLessThan(2, $cutoff->s);
+ }
+
+ public function testSubnet() {
+ // IPv4
+ $this->assertSame(
+ '64.233.191.254/32',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 32])
+ );
+ $this->assertSame(
+ '64.233.191.252/30',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 30])
+ );
+ $this->assertSame(
+ '64.233.191.240/28',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 28])
+ );
+ $this->assertSame(
+ '64.233.191.0/24',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 24])
+ );
+ $this->assertSame(
+ '64.233.188.0/22',
+ $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 22])
+ );
+ // IPv6
+ $this->assertSame(
+ '2001:db8:85a3::8a2e:370:7334/127',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 127])
+ );
+ $this->assertSame(
+ '2001:db8:85a3::8a2e:370:7300/120',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7300', 120])
+ );
+ $this->assertSame(
+ '2001:db8:85a3::/64',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 64])
+ );
+ $this->assertSame(
+ '2001:db8:85a3::/48',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 48])
+ );
+ $this->assertSame(
+ '2001:db8:8500::/40',
+ $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 40])
+ );
+ }
+}
diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php
index 9bde2c664b6..33930a50ce5 100644
--- a/tests/lib/User/SessionTest.php
+++ b/tests/lib/User/SessionTest.php
@@ -9,6 +9,7 @@
namespace Test\User;
+use OC\Security\Bruteforce\Throttler;
use OC\Session\Memory;
use OC\User\User;
@@ -17,15 +18,14 @@ use OC\User\User;
* @package Test\User
*/
class SessionTest extends \Test\TestCase {
-
/** @var \OCP\AppFramework\Utility\ITimeFactory */
private $timeFactory;
-
/** @var \OC\Authentication\Token\DefaultTokenProvider */
protected $tokenProvider;
-
/** @var \OCP\IConfig */
private $config;
+ /** @var Throttler */
+ private $throttler;
protected function setUp() {
parent::setUp();
@@ -36,6 +36,8 @@ class SessionTest extends \Test\TestCase {
->will($this->returnValue(10000));
$this->tokenProvider = $this->getMock('\OC\Authentication\Token\IProvider');
$this->config = $this->getMock('\OCP\IConfig');
+ $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler')
+ ->disableOriginalConstructor()->getMock();
}
public function testGetUser() {
@@ -353,7 +355,6 @@ class SessionTest extends \Test\TestCase {
->getMock();
$session = $this->getMock('\OCP\ISession');
$request = $this->getMock('\OCP\IRequest');
- $user = $this->getMock('\OCP\IUser');
/** @var \OC\User\Session $userSession */
$userSession = $this->getMockBuilder('\OC\User\Session')
@@ -369,8 +370,16 @@ class SessionTest extends \Test\TestCase {
->method('getSystemValue')
->with('token_auth_enforced', false)
->will($this->returnValue(true));
-
- $userSession->logClientIn('john', 'doe', $request);
+ $request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+
+ $userSession->logClientIn('john', 'doe', $request, $this->throttler);
}
public function testLogClientInWithTokenPassword() {
@@ -379,7 +388,6 @@ class SessionTest extends \Test\TestCase {
->getMock();
$session = $this->getMock('\OCP\ISession');
$request = $this->getMock('\OCP\IRequest');
- $user = $this->getMock('\OCP\IUser');
/** @var \OC\User\Session $userSession */
$userSession = $this->getMockBuilder('\OC\User\Session')
@@ -398,8 +406,16 @@ class SessionTest extends \Test\TestCase {
$session->expects($this->once())
->method('set')
->with('app_password', 'I-AM-AN-APP-PASSWORD');
-
- $this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request));
+ $request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+
+ $this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request, $this->throttler));
}
/**
@@ -410,7 +426,6 @@ class SessionTest extends \Test\TestCase {
->disableOriginalConstructor()
->getMock();
$session = $this->getMock('\OCP\ISession');
- $user = $this->getMock('\OCP\IUser');
$request = $this->getMock('\OCP\IRequest');
/** @var \OC\User\Session $userSession */
@@ -433,7 +448,16 @@ class SessionTest extends \Test\TestCase {
->with('john')
->will($this->returnValue(true));
- $userSession->logClientIn('john', 'doe', $request);
+ $request
+ ->expects($this->once())
+ ->method('getRemoteAddress')
+ ->willReturn('192.168.0.1');
+ $this->throttler
+ ->expects($this->once())
+ ->method('sleepDelay')
+ ->with('192.168.0.1');
+
+ $userSession->logClientIn('john', 'doe', $request, $this->throttler);
}
public function testRememberLoginValidToken() {