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.
		
			
				
	
	
		
			188 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			188 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?PHP
 | |
| /**
 | |
|  * Gathers wrappers for handling basic security operations.
 | |
|  */
 | |
| declare(strict_types = 1);
 | |
| 
 | |
| /**
 | |
|  * Gathers wrappers for handling basic security operations.
 | |
|  */
 | |
| final class MD_STD_SEC {
 | |
| 
 | |
|     const REFRESH_TIME_GENERAL = 60; // Time until the comp. with the whole service is cleared.
 | |
|     const REFRESH_TIME_USER = 600;   // Time until the comp. with the same username service is cleared.
 | |
|     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_MULTIPLIER_COMMON   = 1.04;
 | |
|     const BRUTE_FORCE_DELAY_MULTIPLIER_PER_USER = 2.2;
 | |
|     const BRUTE_FORCE_DELAY_MULTIPLIER_PER_IP = 1.8;
 | |
| 
 | |
|     /**
 | |
|      * 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;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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.
 | |
|      *
 | |
|      * @param string  $tool_name          Identifier of the login.
 | |
|      * @param string  $username           Username to look for.
 | |
|      * @param integer $max_execution_time Optional maximum execution time to stay below.
 | |
|      *
 | |
|      * @return boolean
 | |
|      */
 | |
|     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,
 | |
|         // this can't be found out anyway and security is established by _common.
 | |
|         $ip = \filter_var($_SERVER['REMOTE_ADDR'] ?: ($_SERVER['HTTP_X_FORWARDED_FOR'] ?: $_SERVER['HTTP_CLIENT_IP']), \FILTER_VALIDATE_IP) ?: "Failed to find";
 | |
| 
 | |
|         // 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'] > self::REFRESH_TIME_GENERAL) {
 | |
|             $loginLog['common'] = ["count" => 0, "time" => \time()];
 | |
|         }
 | |
|         if (empty($loginLog['usr'][$hash_user]) || \time() - $loginLog['usr'][$hash_user]['time'] > self::REFRESH_TIME_USER) {
 | |
|             $loginLog['usr'][$hash_user] = ["count" => 0, "time" => \time()];
 | |
|         }
 | |
|         if (empty($loginLog['ip'][$hash_ip]) || \time() - $loginLog['ip'][$hash_ip]['time'] > self::REFRESH_TIME_IP) {
 | |
|             $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, MD_STD::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'];
 | |
| 
 | |
|         $delay_micoseconds = self::computeAntiBruteForceDelay($delay_multiplier_common,
 | |
|             $delay_multiplier_per_user,
 | |
|             $delay_multiplier_per_ip,
 | |
|             $max_execution_time);
 | |
| 
 | |
|         // Sleep
 | |
|         \usleep($delay_micoseconds);
 | |
| 
 | |
|         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, worker-src?: 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\'; worker-src ' . ($directives['worker-src'] ?? '\'self\'') . ';';
 | |
| 
 | |
|         if (!empty($frame_ancestors)) {
 | |
|             $policy .= ' frame-ancestors ' . $frame_ancestors . ';';
 | |
|         }
 | |
| 
 | |
|         header($policy);
 | |
| 
 | |
|     }
 | |
| }
 |