MD_STD/MD_STD_SEC.php

146 lines
5.9 KiB
PHP

<?PHP
/**
* Gathers wrappers for handling basic security operations.
*/
declare(strict_types = 1);
/**
* Class providing static functions with basic security operations.
*/
final class MD_STD_SEC {
/**
* Function for retrieving the anti-csrf token or generating it if need be.
*
* @return string
*/
public static function getAntiCsrfToken():string {
if (empty($_SESSION['csrf-token'])) {
$_SESSION['csrf-token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf-token'];
}
/**
* Function for validating anti-csrf tokens. Each anti-csrf token is removed
* after use.
*
* @return boolean
*/
public static function validateAntiCsrfToken():bool {
$validity = false;
if (!empty($_POST['csrf-token'])
&& !empty($_SESSION['csrf-token'])
&& hash_equals($_SESSION['csrf-token'], $_POST['csrf-token']) === true
) {
$validity = true;
}
$_SESSION['csrf-token'] = null; unset($_SESSION['csrf-token']);
return $validity;
}
const BRUTE_FORCE_DELAY_DEFAULT = 2000; // 2000 microseconds = 2 milliseconds
const BRUTE_FORCE_DELAY_MULTIPLIER_COMMON = 1.08;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 3;
const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 8;
/**
* Prevent brute force attacks by delaying the login .
*
* @param string $tool_name Identifier of the login.
* @param string $username Username to look for.
*
* @return boolean
*/
public static function preventBruteForce(string $tool_name, string $username):bool {
// 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.
$ip = \strval($_SERVER['REMOTE_ADDR'] ?: ($_SERVER['HTTP_X_FORWARDED_FOR'] ?: $_SERVER['HTTP_CLIENT_IP']));
// Set name of log file
$logfile_common = \sys_get_temp_dir() . "/logins_{$tool_name}.json";
// Ensure the log files exist
if (!\file_exists($logfile_common)) \file_put_contents($logfile_common, "[]");
// Hash entered username and IP to prevent malicious strings from
// entering the system.
$hash_user = \md5($username);
$hash_ip = \md5($ip);
// Delete the log files if they are too old
$loginLog = \json_decode(MD_STD::file_get_contents($logfile_common), \true) ?: [];
// Ensure the counters exist and aren't old than 600 seconds / 10 minutes
if (empty($loginLog['common']) || \time() - $loginLog['common']['time'] > 600) {
$loginLog['common'] = ["count" => 0, "time" => \time()];
}
if (empty($loginLog['usr'][$hash_user]) || \time() - $loginLog['usr'][$hash_user]['time'] > 600) {
$loginLog['usr'][$hash_user] = ["count" => 0, "time" => \time()];
}
if (empty($loginLog['ip'][$hash_ip]) || \time() - $loginLog['ip'][$hash_ip]['time'] > 600) {
$loginLog['ip'][$hash_ip] = ["count" => 0, "time" => \time()];
}
// Increase counters and update timers
$loginLog['common']['count']++;
$loginLog['common']['time'] = \time();
$loginLog['usr'][$hash_user]['count']++;
$loginLog['usr'][$hash_user]['time'] = \time();
$loginLog['ip'][$hash_ip]['count']++;
$loginLog['ip'][$hash_ip]['time'] = \time();
// Update the log file
\file_put_contents($logfile_common, \json_encode($loginLog));
// Translate counters into delay multipliers
$delay_multiplier_common = $loginLog['common']['count'];
$delay_multiplier_per_user = $loginLog['usr'][$hash_user]['count'];
$delay_multiplier_per_ip = $loginLog['ip'][$hash_ip]['count'];
// Calculate delay
$delay_micoseconds = \intval(self::BRUTE_FORCE_DELAY_DEFAULT *
(self::BRUTE_FORCE_DELAY_MULTIPLIER_COMMON ** $delay_multiplier_common) *
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER ** $delay_multiplier_per_user) *
(self::BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP ** $delay_multiplier_per_ip));
$max_execution_microseconds = \abs((int)\ini_get("max_execution_time")) * 1000000;
// Sleep
\usleep(min($delay_micoseconds, \abs($max_execution_microseconds - 1000000)));
if ($delay_micoseconds > \abs($max_execution_microseconds - 1000000)) {
return false;
}
return true;
}
/**
* Send CSP headers.
*
* @param array{default-src: string, connect-src: string, script-src: string, img-src: string, media-src: string, style-src: string, frame-src: string, object-src: string, base-uri: string, form-action: string, frame-ancestors?: string} $directives Directives to send. Font source is always set to 'self', and hence excluded.
* @param string $frame_ancestors Frame ancestors directive. Default is to not set it.
*
* @return void
*/
public static function sendContentSecurityPolicy(array $directives, string $frame_ancestors = ""):void {
$policy = 'Content-Security-Policy: default-src ' . $directives['default-src'] . '; connect-src ' . $directives['connect-src'] . '; script-src ' . $directives['script-src'] . '; img-src ' . $directives['img-src'] . '; media-src ' . $directives['media-src'] . '; style-src ' . $directives['style-src'] . '; font-src \'self\'; frame-src ' . $directives['frame-src'] . '; object-src ' . $directives['object-src'] . '; base-uri ' . $directives['base-uri'] . '; form-action ' . $directives['form-action'] . '; manifest-src \'self\';';
if (!empty($frame_ancestors)) {
$policy .= ' frame-ancestors ' . $frame_ancestors . ';';
}
header($policy);
}
}