sanitize($input)); // Clean up a path to prevent argument injection. case 'command': if (!utf8_check($input)) return false; if (\RX_WINDOWS || preg_match('![^a-z0-9/._-]!', $input)) return escapeshellarg($input); return strval($input); // Unknown filters. default: throw new Exception('Unknown filter type for sanitize: ' . $type); } } /** * Encrypt a string using AES. * * @param string $plaintext * @param string $key (optional) * @return string */ public static function encrypt(string $plaintext, ?string $key = null): string { // Get the encryption key. $key = $key ?: config('crypto.encryption_key'); $key = substr(hash('sha256', $key, true), 0, 16); // Encrypt in a format that is compatible with defuse/php-encryption 1.2.x. return base64_encode(\CryptoCompat::encrypt($plaintext, $key)); } /** * Decrypt a string using AES. * * @param string $plaintext * @param string $key (optional) * @return string|false */ public static function decrypt(string $ciphertext, ?string $key = null) { // Get the encryption key. $key = $key ?: config('crypto.encryption_key'); $key = substr(hash('sha256', $key, true), 0, 16); // Check whether the ciphertext is valid. $ciphertext = @base64_decode($ciphertext); if (strlen($ciphertext) < 48) { return false; } // Decrypt in a format that is compatible with defuse/php-encryption 1.2.x. return \CryptoCompat::decrypt($ciphertext, $key); } /** * Create a digital signature to verify the authenticity of a string. * * @param string $string * @return string */ public static function createSignature(string $string): string { $key = config('crypto.authentication_key'); $salt = self::getRandom(8, 'alnum'); $hash = substr(base64_encode(hash_hmac('sha256', hash_hmac('sha256', $string, $salt), $key, true)), 0, 32); return $salt . strtr($hash, '+/', '-_'); } /** * Check whether a signature is valid. * * @param string $string * @param string $signature * @return bool */ public static function verifySignature(string $string, string $signature): bool { if(strlen($signature) !== 40) { return false; } $key = config('crypto.authentication_key'); $salt = substr($signature, 0, 8); $hash = substr(base64_encode(hash_hmac('sha256', hash_hmac('sha256', $string, $salt), $key, true)), 0, 32); return self::compareStrings(substr($signature, 8), strtr($hash, '+/', '-_')); } /** * Generate a cryptographically secure random string. * * @param int $length * @param string $format * @return string */ public static function getRandom(int $length = 32, string $format = 'alnum'): string { // Find out how many bytes of entropy we really need. switch($format) { case 'binary': $entropy_required_bytes = $length; break; case 'hex': $entropy_required_bytes = ceil($length / 2); break; case 'alnum': case 'printable': default: $entropy_required_bytes = ceil($length * 3 / 4); break; } // Cap entropy to 256 bits from any one source, because anything more is meaningless. $entropy_capped_bytes = min(32, $entropy_required_bytes); $entropy = false; // Find and use the most secure way to generate a random string. if(function_exists('random_bytes')) { try { $entropy = random_bytes($entropy_capped_bytes); } catch (\Exception $e) { $entropy = false; } } // Use other good sources of entropy if random_bytes() is not available. if ($entropy === false) { if(function_exists('openssl_random_pseudo_bytes')) { $entropy = openssl_random_pseudo_bytes($entropy_capped_bytes); } elseif(function_exists('mcrypt_create_iv') && !\RX_WINDOWS) { $entropy = mcrypt_create_iv($entropy_capped_bytes, \MCRYPT_DEV_URANDOM); } elseif(function_exists('mcrypt_create_iv') && \RX_WINDOWS) { $entropy = mcrypt_create_iv($entropy_capped_bytes, \MCRYPT_RAND); } elseif(!\RX_WINDOWS && @is_readable('/dev/urandom')) { $fp = fopen('/dev/urandom', 'rb'); if (function_exists('stream_set_read_buffer')) // This function does not exist in HHVM. { stream_set_read_buffer($fp, 0); // Prevent reading several KB of unnecessary data from urandom. } $entropy = fread($fp, $entropy_capped_bytes); fclose($fp); } } // Use built-in source of entropy if an error occurs while using other functions. if($entropy === false || strlen($entropy) < $entropy_capped_bytes) { $entropy = ''; for($i = 0; $i < $entropy_capped_bytes; $i += 2) { $entropy .= pack('S', rand(0, 65536) ^ mt_rand(0, 65535)); } } // Mixing (see RFC 4086 section 5) $output = ''; for($i = 0; $i < $entropy_required_bytes; $i += 32) { $output .= hash('sha256', $entropy . $i . rand(), true); } // Encode and return the random string. switch($format) { case 'binary': return substr($output, 0, $length); case 'printable': $salt = ''; for($i = 0; $i < $length; $i++) { $salt .= chr(33 + (crc32(sha1($i . $output)) % 94)); } return $salt; case 'hex': return substr(bin2hex($output), 0, $length); case 'alnum': default: $salt = substr(base64_encode($output), 0, $length); $replacements = chr(rand(65, 90)) . chr(rand(97, 122)) . rand(0, 9); return strtr($salt, '+/=', $replacements); } } /** * Generate a cryptographically secure random number between $min and $max. * * @param int $min * @param int $max * @return int */ public static function getRandomNumber(int $min = 0, int $max = \PHP_INT_MAX): int { if (function_exists('random_int')) { return random_int($min, $max); } else { $bytes_required = min(4, ceil(log($max - $min, 2) / 8) + 1); $bytes = self::getRandom($bytes_required, 'binary'); $offset = abs(hexdec(bin2hex($bytes)) % ($max - $min + 1)); return intval($min + $offset); } } /** * Generate a random UUID. * * The code for UUIDv4 is based on https://stackoverflow.com/a/15875555/481206 * * @param int $version (4 or 7) * @return string */ public static function getRandomUUID(int $version = 4): string { if ($version === 4) { $randpool = self::getRandom(16, 'binary'); $randpool[6] = chr(ord($randpool[6]) & 0x0f | 0x40); $randpool[8] = chr(ord($randpool[8]) & 0x3f | 0x80); } elseif ($version === 7) { $timestamp = microtime(false); $timestamp = substr(pack('J', (intval(substr($timestamp, -10), 10) * 1000) + intval(substr($timestamp, 2, 3), 10)), -6); $randpool = $timestamp . self::getRandom(10, 'binary'); $randpool[6] = chr(ord($randpool[6]) & 0x0f | 0x70); $randpool[8] = chr(ord($randpool[8]) & 0x3f | 0x80); } else { throw new Exception('Invalid UUID version: ' . $version); } return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($randpool), 4)); } /** * Compare two strings in constant time. * * @param string $a * @param string $b * @return bool */ public static function compareStrings(string $a, string $b): bool { if(function_exists('hash_equals')) { return hash_equals($a, $b); } $diff = strlen($a) ^ strlen($b); $maxlen = min(strlen($a), strlen($b)); for($i = 0; $i < $maxlen; $i++) { $diff |= ord($a[$i]) ^ ord($b[$i]); } return $diff === 0; } /** * Check if the current request seems to be a CSRF attack. * * This method returns true if the request seems to be innocent, * and false if it seems to be a CSRF attack. * * @param string $referer (optional) * @return bool */ public static function checkCSRF(?string $referer = null): bool { // If CSRF token checking is enabled, check the token header or parameter first. // Tokens are allowed to be missing for unauthenticated users. $check_csrf_token = config('security.check_csrf_token') ? true : false; if ($check_csrf_token) { $token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_REQUEST['_rx_csrf_token'] ?? null); if ($token) { return Session::verifyToken((string)$token, '', $check_csrf_token); } elseif (Session::getMemberSrl()) { trigger_error('CSRF token missing in authenticated POST request: ' . (\Context::get('act') ?: '(no act)'), \E_USER_WARNING); return false; } } elseif ($token = isset($_REQUEST['_fb_adsense_token']) ? $_REQUEST['_fb_adsense_token'] : null) { return Password::checkPassword($token, 'bb15471de21f33c373abbea6438730ace9bbbacf5f4f9a0cbebdfff7e99c50fe631a78efe3e39736836b5b2082a0c3939e4c4e0f0f2e0028042411c4a8797b73'); } // Return early for GET and other "safe" requests. if (in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD', 'OPTIONS', 'TRACE'])) { return false; } // The Sec-Fetch-Site header is the standard way to check whether the request is same-origin. $sec_fetch_site = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? ''; if ($sec_fetch_site === 'same-origin' || $sec_fetch_site === 'none') { return true; } if ($sec_fetch_site === 'cross-site') { return false; } // If the Sec-Fetch-Site header is not available, check the Origin header. $origin = strval($_SERVER['HTTP_ORIGIN'] ?? ''); if ($origin !== '' && $origin !== 'null') { return URL::isInternalURL($origin); } // If the Origin header is not available, fall back to the Referer header. // The Referer header is NOT allowed to be empty in this case. $referer = $referer ?? strval($_SERVER['HTTP_REFERER'] ?? ''); if ($referer !== '' && $referer !== 'null') { return URL::isInternalURL($referer); } else { return false; } } /** * Check if the current request seems to be an XXE (XML external entity) attack. * * This method returns true if the request seems to be innocent, * and false if it seems to be an XXE attack. * This is the opposite of XE's Security::detectingXEE() method. * The name has also been changed to the more accurate acronym XXE. * * @param string $xml (optional) * @return bool */ public static function checkXXE(?string $xml = null): bool { // Stop if there is no XML content. if (!$xml) { return true; } // Reject entity tags. if (strpos($xml, '/s', '', substr($xml, 0, 100), 1); if (($xml = trim(substr_replace($xml, $header, 0, 100))) === '') { return false; } // Check if there is no content after the DTD. $header = preg_replace('/^]*+>/i', '', substr($xml, 0, 200), 1); if (($xml = trim(substr_replace($xml, $header, 0, 200))) === '') { return false; } // Check that the root tag is valid. if (!preg_match('/^<(methodCall|methodResponse|fault)/', $xml)) { return false; } return true; } }