Set a cap to maximum delay in preventing brute force attacks

This is necessary because PHP-FPM fails if sleep / usleep runs beyond
the maximum execution time of php.ini, leading to whole vhosts falling
over.
This commit is contained in:
Joshua Ramon Enslin 2022-08-14 13:08:40 +02:00
parent f477401114
commit 52aeedd31e
Signed by: jrenslin
GPG Key ID: 46016F84501B70AE
2 changed files with 86 additions and 15 deletions

View File

@ -14,9 +14,9 @@ final class MD_STD_SEC {
const REFRESH_TIME_IP = 180; // Time until the comp. with the same IP is cleared. This should be lower than the user-level one, as people working together may be using a common IP. const REFRESH_TIME_IP = 180; // Time until the comp. with the same IP is cleared. This should be lower than the user-level one, as people working together may be using a common IP.
const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds
const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.08; const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.04;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 2.8; const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 2.2;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 2; const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 1.8;
/** /**
* Function for retrieving the anti-csrf token or generating it if need be. * Function for retrieving the anti-csrf token or generating it if need be.
@ -55,15 +55,52 @@ final class MD_STD_SEC {
} }
/**
* Computes the delay for preventing brute force attacks.
*
* @param integer $counter_common General counter.
* @param integer $counter_by_user Username-specific counter.
* @param integer $counter_by_ip IP-specific counter.
* @param integer $max Optional script-specific maximum execution time.
*
* @return integer
*/
public static function computeAntiBruteForceDelay(int $counter_common, int $counter_by_user, int $counter_by_ip, int $max = 0):int {
$counter_common = min($counter_common, 40);
$counter_by_user = min($counter_by_user, 40);
$counter_by_ip = min($counter_by_ip, 40);
// Calculate delay
$delay_micoseconds = \abs(\intval(self::BRUTE_FORCE_DELAY_DEFAULT *
(self::BRUTE_FORCE_DELAY_MULTIPLIER_COMMON ** $counter_common) *
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER ** $counter_by_user) *
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP ** $counter_by_ip)));
$max_execution_time = \abs((int)\ini_get("max_execution_time"));
if ($max_execution_time !== 0) {
$max_execution_microseconds = $max_execution_time * 1000000;
$delay_micoseconds = min($delay_micoseconds, \abs($max_execution_microseconds - 1000000));
}
if ($max !== 0 && $max * 1000000 < $delay_micoseconds) {
$delay_micoseconds = max($max - 1, 1) * 1000000;
}
return rand($delay_micoseconds - 100000, $delay_micoseconds);
}
/** /**
* Prevent brute force attacks by delaying the login. * Prevent brute force attacks by delaying the login.
* *
* @param string $tool_name Identifier of the login. * @param string $tool_name Identifier of the login.
* @param string $username Username to look for. * @param string $username Username to look for.
* @param integer $max_execution_time Optional maximum execution time to stay below.
* *
* @return boolean * @return boolean
*/ */
public static function preventBruteForce(string $tool_name, string $username):bool { public static function preventBruteForce(string $tool_name, string $username, int $max_execution_time = 0):bool {
// Unstable but working way to get the user's IP. If the IP is falsified, // Unstable but working way to get the user's IP. If the IP is falsified,
// this can't be found out anyway and security is established by _common. // this can't be found out anyway and security is established by _common.
@ -112,16 +149,13 @@ final class MD_STD_SEC {
$delay_multiplier_per_user = $loginLog['usr'][$hash_user]['count']; $delay_multiplier_per_user = $loginLog['usr'][$hash_user]['count'];
$delay_multiplier_per_ip = $loginLog['ip'][$hash_ip]['count']; $delay_multiplier_per_ip = $loginLog['ip'][$hash_ip]['count'];
// Calculate delay $delay_micoseconds = self::computeAntiBruteForceDelay($delay_multiplier_common,
$delay_micoseconds = \intval(self::BRUTE_FORCE_DELAY_DEFAULT * $delay_multiplier_per_user,
(self::BRUTE_FORCE_DELAY_MULTIPLIER_COMMON ** $delay_multiplier_common) * $delay_multiplier_per_ip,
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER ** $delay_multiplier_per_user) * $max_execution_time);
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP ** $delay_multiplier_per_ip));
$max_execution_microseconds = \abs((int)\ini_get("max_execution_time")) * 1000000;
// Sleep // Sleep
\usleep(min($delay_micoseconds, \abs($max_execution_microseconds - 1000000))); \usleep($delay_micoseconds);
if ($delay_micoseconds > \abs($max_execution_microseconds - 1000000)) { if ($delay_micoseconds > \abs($max_execution_microseconds - 1000000)) {
return false; return false;

37
tests/MD_STD_SECTest.php Normal file
View File

@ -0,0 +1,37 @@
<?PHP
/**
* Tests for MD_STD_SEC.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
*/
declare(strict_types = 1);
use PHPUnit\Framework\TestCase;
require __DIR__ . '/../src/MD_STD_SEC.php';
/**
* Tests for MD_STD_SEC.
*/
final class MD_STD_SECTest extends TestCase {
/**
* Function for testing if the page can be opened using invalid values for objektnum.
*
* @author Joshua Ramon Enslin <joshua@museum-digital.de>
* @group MissingInputs
* @group SafeForProduction
*
* @return void
*/
public function testComputeAntiBruteForceDelayDoesNotGoOverMax():void {
$delay = MD_STD_SEC::computeAntiBruteForceDelay(100, 100, 100);
self::assertGreaterThan(0, $delay);
# self::assertLessThan(10 * 1000000, $delay); // Smaller than 10 seconds
$delay_reduced = MD_STD_SEC::computeAntiBruteForceDelay(100, 100, 100, 3);
self::assertGreaterThan(0, $delay_reduced);
self::assertLessThan(3 * 1000000, $delay_reduced); // Smaller than 10 seconds
}
}