Skip to content

Commit

Permalink
- Added functions for fixed execution times to mitigate timing attacks
Browse files Browse the repository at this point in the history
       with parameters and docs
- Test of DbalGateway added
- doc/Installation.md added keepCountsFor parameter to configurations
  • Loading branch information
metaclass-nl committed May 9, 2015
1 parent 1564c07 commit 0810088
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 13 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ RELEASE NOTES

This is a pre-release version under development.

May be vurnerable to user enumeration through timing attacks because of differences in database query performance
for frequently and infrequently used usernames,
May be vurnerable to enumeration of usernames through timing attacks because of
differences in database query performance for frequently and infrequently used usernames.
This can be mitigated by calling ::sleepUntilFixedExecutionTime. Under normal circomstances
that should be sufficient if the fixedExecutionSeconds is set long enough, but under
high (database) server loads when performance degrades, under specific conditons
information may still be extractable by timing.

DOCUMENTATION
-------------
Expand Down
25 changes: 24 additions & 1 deletion doc/Counting and deciding.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,30 @@ Not used: If the user has been released for the cookieToken (but not on the IP a
made from the same cookieToken. If the total is higher then the 'limitPerUserName' setting, it will throw
UsernameBlockedForCookieException.

All these exceptions inherit from AuthenticationBlockedException.
All these exceptions inherit from AuthenticationBlockedException.

Waiting
-------

Though the actual counting is done by the database, adding up more counters may
still take more time then when for example no counters exist. If correct user names
do not have the same frequency as incorrect ones, an attacker may draw conclusions
from execution time differences.

This can be mitigated by calling ::sleepUntilFixedExecutionTime. This function
(and the underlying ::sleepUntilSinceInit) will try to sleep until a fixed
execution time has passed since ::init was called.

Because of doubts about the accurateness of microtime() and to hide system clock
details a random between 0 and randomSleepingNanosecondsMax nanoseconds is added.

Under high (database) server loads when performance degrades, the fixed execution time
$seconds may already have passed before ::sleepUntilSinceInit is called.
Therefore sleeping will be until the next whole multitude of $seconds
has passed. I.e. if $seconds is 0.9 and one second has passed, sleeping will be
until 1.8. Generally this will again hide the what the governor (and the database)
has been doing, but in borderline conditions information may still leak.


Improvements
------------
Expand Down
18 changes: 17 additions & 1 deletion doc/Installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ From your own application:
'blockIpAddressesFor' => "17 minutes", // actual blocking for up to counterDurationInSeconds shorter!
'limitBasePerIpAddress' => 10,
'releaseUserOnLoginSuccess' => false,
'allowReleasedUserOnAddressFor' => "30 days" );
'allowReleasedUserOnAddressFor' => "30 days",
'keepCountsFor' => '4 days',
'fixedExecutionSeconds' => 0.1);
```

Configurations
Expand Down Expand Up @@ -221,6 +223,20 @@ Configurations
garbage collected, but if allowReleasedUserOnAddressFor (or allowReleasedUserByCookieFor)
is set to a longer duration, the releases will be kept longer (according to the longest one).

9. Fixed execution time

fixedExecutionSeconds

Fixed execution time in order to mitigate timing attacks. To apply, call ::sleepUntilFixedExecutionTime.

10. Maximum random sleeping time in nanoseconds

randomSleepingNanosecondsMax

Because of doubts about the accurateness of microtime() and to hide system clock
details a random between 0 and this value is added by ::sleepUntilSinceInit (which
is called by ::sleepUntilFixedExecutionTime).

Notes

- releasing is possible for a username in general, an IP address in general, or for the combination of a username with an ip address
Expand Down
1 change: 1 addition & 0 deletions doc/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ comitted and pushed
---------------------
- TresholdsGovernorTest adapted to use of keepCountsFor parameter
- doc/Installation.md added keepCountsFor parameter to configurations
comiited, pushed
-------------------
(blocked columns and statistics need tests and documentation,
statistics UI need refactoring from UserBundle)
77 changes: 69 additions & 8 deletions src/Metaclass/TresholdsGovernor/Service/TresholdsGovernor.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,17 @@ class TresholdsGovernor {
* past blockings if a user ask questions when he/she can't log in.
* This setting should never be set lower then $blockUsernamesFor and $blockIpAddressesFor */
public $keepCountsFor = '4 days';


/** @var float fixed execution time in order to mitigate timing attacks. To apply, call sleepUntilFixedExecutionTime */
public $fixedExecutionSeconds = 0.1;

/** @var int Maximum random nanoseconds sleeping time */
public $randomSleepingNanosecondsMax = 99999;

//variables
/** @var float microtime of init */
protected $initMicrotime;

/** @var string $ipAddress IP Address sending the request that is being processed */
protected $ipAddress;

Expand All @@ -78,7 +87,7 @@ class TresholdsGovernor {
/** @var int $failureCountForUserName Total number of failures counted by $this->username within the $this->blockUsernamesFor duration */
protected $failureCountForUserName;

/** @var boolean $isUserReleasedOnAddress Wheater $this->username has been released for $this->ipAddress within the $this->allowReleasedUserOnAddressFor duration */
/** @var boolean $isUserReleasedOnAddress Weather $this->username has been released for $this->ipAddress within the $this->allowReleasedUserOnAddressFor duration */
public $isUserReleasedOnAddress = false;

/** @var int $failureCountForUserOnAddress Total number of failures counted by the combination of both $this->username and $this->ipAddress within the $this->blockUsernamesFor duration */
Expand Down Expand Up @@ -122,21 +131,22 @@ protected function setPropertiesFromParams($params)

/**
* Initializes this with the supplied parameters and the counts and booleans calculated with the parameters.
* Null paramter values are processed as empty strings.
* Null parameter values are processed as empty strings.
* @param string $ipAddress IP Address sending the request that is being processed
* @param string $username username from the request that is being processed
* @param string $password not used
* @param string $cookieToken token from the cookie from the request that is being processed
*/
public function initFor($ipAddress, $username, $password, $cookieToken)
{
$this->initMicrotime = microtime(true);

//cast to string because null is used for control in some Gateway functions
$this->ipAddress = (string) $ipAddress;
$this->username = (string) $username;
$this->cookieToken = (string) $cookieToken;
//$this->password = (string) $password;



$timeLimit = new \DateTime("$this->dtString - $this->blockIpAddressesFor");
$this->failureCountForIpAddress = $this->requestCountsManager->countLoginsFailedForIpAddres($ipAddress, $timeLimit);

Expand All @@ -159,8 +169,8 @@ public function initFor($ipAddress, $username, $password, $cookieToken)
}

/**
* Decides wheather or not to block the current request amd registers failure on blocking
* @param boolean $justFailed Wheather the login has already failed (for reasons external to this governor)
* Decides wheater or not to block the current request amd registers failure on blocking
* @param boolean $justFailed Wheater the login has already failed (for reasons external to this governor)
* but is not yet registered as a failure. Default is false.
* @return \Metaclass\TresholdsGovernor\Result\Rejection or null if the governor does not require the login to be blocked.
* (Blocking may still take place for reasons external to this governor)
Expand All @@ -183,7 +193,7 @@ public function checkAuthentication($justFailed=false)
}

/**
* Decides wheather or not to block the current request.
* Decides wheater or not to block the current request.
* @param boolean $justFailed Wheather the login has already failed (for reasons external to this governor)
* but is not yet registered as a failure. Default is false.
* @return \Metaclass\TresholdsGovernor\Result\Rejection or null if the governor does not require the login to be blocked.
Expand Down Expand Up @@ -309,6 +319,57 @@ public function getMinBlockingLimit()
$addressLimit = new \DateTime("$this->dtString - $this->blockIpAddressesFor");
return min($usernameLimit, $addressLimit);
}

/** @return float seconds that have passed since init was called,
* accurate to microseconds */
public function getSecondsPassedSinceInit()
{
return microtime(true) - $this->initMicrotime;
}

/** Function to reach fixed execution time in order to mitigate timing attacks.
* Because of doubts about the accurateness of microtime() and to hide system clock details
* a random between 0 and randomSleepingNanosecondsMax nanoseconds is added.
* Because the time <until> may in fact be in the past, sleeping will be
* until next whole multitude of $seconds has passed. I.e. if $seconds is
* 0.9 and one second has passed, sleeping will be until 1.8
* @param float $seconds since ::init until when to sleep
*/
public function sleepUntilSinceInit($seconds)
{
$passed = $this->getSecondsPassedSinceInit();
$multiplier = ceil($passed/$seconds);
$multitude = $multiplier * $seconds;
$toSleep = $multitude - $passed;
$wholeSeconds = floor($toSleep);
$nanoSeconds = round(($toSleep - $wholeSeconds) * 1000000000);

// Add random nanoseconds sleeping time
$nanoSeconds += mt_rand(0, $this->randomSleepingNanosecondsMax);
if ($nanoSeconds > 1000000000) {
$nanoSeconds -= 1000000000;
$wholeSeconds++;
}

do {
$result = time_nanosleep($wholeSeconds, $nanoSeconds);
if (is_array($result)) {
$wholeSeconds = $result['seconds'];
$nanoSeconds = $result['nanoseconds'];
}
} while (is_array($result));
}

/**
* Function to reach fixed execution time of the tresholds governor in order to
* mitigate timing attacks. Typically used if authentication is blocked.
* If authentication really takes place, more time will be needed because
* of password hashing. Then ::sleepUntilSinceInit may be used with a custom value.
*/
public function sleepUntilFixedExecutionTime()
{
$this->sleepUntilSinceInit($this->fixedExecutionSeconds);
}
}

?>
98 changes: 98 additions & 0 deletions src/Metaclass/TresholdsGovernor/Tests/Gateway/DbalGatewayTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
//copyright (c) MetaClass Groningen 2014

namespace Metaclass\TresholdsGovernor\Tests\Gateway;

use Metaclass\TresholdsGovernor\Gateway\DbalGateway;

use \Doctrine\DBAL\Configuration;
use \Doctrine\DBAL\DriverManager;
use \Metaclass\TresholdsGovernor\Tests\Mock\RecordingWrapper;

class DbalGatewayTest extends \PHPUnit_Framework_TestCase {

STATIC $connection;

protected $wrapper, $gateway, $requestData1;
function setup()
{
if (!isSet(self::$connection)) {
$config = new Configuration();
$connectionParams = array(
'memory ' => true,
'driver' => 'pdo_sqlite',
);
self::$connection = DriverManager::getConnection($connectionParams, $config);
}
$this->wrapper = new RecordingWrapper(self::$connection);
$this->gateway = new DbalGateway($this->wrapper);

$this->requestData1 = array(
'dtFrom' => '2001-08-12 15:33:08',
'ipAddress' => '122.2.3.4',
'username' => 'gateway_test_user',
'token' => 'gateway_test_cookie',
);
}

function test_createTables()
{
$sql = "
CREATE TABLE `secu_requests` (
`id` INTEGER PRIMARY KEY, -- alias for ROWID, like auto_increment
`dtFrom` datetime NOT NULL,
`username` varchar(25) NOT NULL,
`ipAddress` varchar(25) NOT NULL,
`cookieToken` varchar(40) NOT NULL,
`loginsFailed` int(11) NOT NULL DEFAULT '0',
`loginsSucceeded` int(11) NOT NULL DEFAULT '0',
`ipAddressBlocked` int(11) NOT NULL DEFAULT '0',
`usernameBlocked` int(11) NOT NULL DEFAULT '0',
`usernameBlockedForIpAddress` int(11) NOT NULL DEFAULT '0',
`usernameBlockedForCookie` int(11) NOT NULL DEFAULT '0',
`requestsAuthorized` int(11) NOT NULL DEFAULT '0',
`requestsDenied` int(11) NOT NULL DEFAULT '0',
`userReleasedAt` datetime DEFAULT NULL,
`addressReleasedAt` datetime DEFAULT NULL,
`userReleasedForAddressAndCookieAt` datetime DEFAULT NULL
) ;
";
self::$connection ->executeUpdate($sql);
self::$connection ->executeUpdate("CREATE INDEX `byDtFrom` ON secu_requests(`dtFrom`)");
self::$connection ->executeUpdate("CREATE INDEX `byUsername` ON secu_requests(`username`,`dtFrom`,`userReleasedAt`)");
self::$connection ->executeUpdate("CREATE INDEX `byAddress` ON secu_requests(`ipAddress`,`dtFrom`,`addressReleasedAt`)");
self::$connection ->executeUpdate("CREATE INDEX `byUsernameAndAddress` ON secu_requests(`username`,`ipAddress`,`dtFrom`,`userReleasedForAddressAndCookieAt`)");
}

function test_createRequestCountsWith()
{
$loginSucceeded = false;
$blockedCounterName = null;
$this->gateway->insertOrIncrementCount(new \DateTime($this->requestData1['dtFrom']), $this->requestData1['username'], $this->requestData1['ipAddress'], $this->requestData1['token'], $loginSucceeded, $blockedCounterName);

$call = $this->wrapper->calls[0];
$qb = $call[2];
$this->assertEquals('createQueryBuilder', $call[0]);
$this->assertEquals("SELECT r.id FROM secu_requests r WHERE (r.username = :username) AND (r.ipAddress = :ipAddress) AND (r.dtFrom = :dtFrom) AND (r.cookieToken = :token) AND (addressReleasedAt IS NULL) AND (userReleasedAt IS NULL) AND (userReleasedForAddressAndCookieAt IS NULL)"
, $qb->getSQL());
$this->assertEquals($this->requestData1, $qb->getParameters(), 'parameters');

$result = self::$connection->fetchAll("SELECT * FROM secu_requests");
$this->assertEquals(1, count($result), '1 row');
$this->assertEquals($this->requestData1['dtFrom'], $result[0]['dtFrom'], 'dtFrom');
$this->assertEquals($this->requestData1['username'], $result[0]['username'], 'username');
$this->assertEquals($this->requestData1['ipAddress'], $result[0]['ipAddress'], 'ipAddress');
$this->assertEquals($this->requestData1['token'], $result[0]['cookieToken'], 'cookieToken');
$this->assertEquals(1, $result[0]['loginsFailed'], 'loginsFailed');
$this->assertEquals(0, $result[0]['loginsSucceeded'], 'loginsSucceeded');
$this->assertEquals(0, $result[0]['ipAddressBlocked'], 'ipAddressBlocked');
$this->assertEquals(0, $result[0]['usernameBlocked'], 'usernameBlocked');
$this->assertEquals(0, $result[0]['usernameBlockedForIpAddress'], 'usernameBlockedForIpAddress');
$this->assertEquals(0, $result[0]['usernameBlockedForCookie'], 'usernameBlockedForCookie');
$this->assertEquals(0, $result[0]['requestsAuthorized'], 'requestsAuthorized');
$this->assertEquals(0, $result[0]['requestsDenied'], 'requestsDenied');
$this->assertNull($result[0]['userReleasedAt'], 'userReleasedAt');
$this->assertNull($result[0]['addressReleasedAt'], 'addressReleasedAt');
$this->assertNull($result[0]['userReleasedForAddressAndCookieAt'], 'userReleasedForAddressAndCookieAt');
}
}
25 changes: 25 additions & 0 deletions src/Metaclass/TresholdsGovernor/Tests/Mock/RecordingWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
//copyright (c) MetaClass Groningen 2014

namespace Metaclass\TresholdsGovernor\Tests\Mock;


class RecordingWrapper {

protected $wrapped;
public $calls;

public function __construct($wrapped)
{
$this->wrapped = $wrapped;
$this->calls = array();
}

public function __call($method, $arguments)
{
$result = call_user_func_array(array($this->wrapped, $method), $arguments);
$this->calls[] = array($method, $arguments, $result);
return $result;
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<?php
<?php
//copyright (c) MetaClass Groningen 2014

namespace Metaclass\TresholdsGovernor\Tests\Service;

use Metaclass\TresholdsGovernor\Service\TresholdsGovernor;
Expand Down

0 comments on commit 0810088

Please sign in to comment.