You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
IOcornerstone/src/Framework/Security.php

312 lines
10 KiB

<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\{
Configure,
Requires
};
/**
* Description of Security
*
* @author Robert Strutts
*/
class Security
{
use \IOcornerstone\Framework\Trait\Security\CsrfTokenFunctions;
use \IOcornerstone\Framework\Trait\Security\SessionHijackingFunctions;
/**
* Get unique IDs for database
* @return int
*/
public static function getUniqueNumber(): int {
return abs(crc32(microtime()));
}
/**
* Get token
* @return string
*/
public static function getUniqueId(): string {
$moreEntropy = true;
$prefix = ""; // Blank is a rand string
return md5(uniqid($prefix, $moreEntropy));
}
public static function useHmac(string $algo, string $pepper) {
if (!function_exists("hash_hmac_algos")) {
throw new \Exception("hash_hmac not installed!");
}
if (strpos($algo, "md") !== false) {
throw new \Exception("MD is too weak!");
}
if (strpos($algo, "sha1") !== false) {
throw new \Exception("sha1 is too weak!");
}
$allowed = hash_hmac_algos();
if (in_array(strtolower($algo), $allowed)) {
return hash_hmac($algo, $pepper);
}
throw new \Exception("hmac algo not found!");
}
/*
* Consider MD5 and SHA1 which are fast and efficient,
* making them ideal for check summing and file verification.
* However, their speed makes them unsuitable for hashing a
* user’s password. With today’s computational power of
* modern CPUs/GPUs and cloud computing, a hashed password
* can be cracked by brute force in a matter of minutes.
* It’s fairly trivial to quickly generate billions of MD5
* hashes from random words until a match is found, thereby
* revealing the original plain-text password. Instead,
* intentionally slower hashing algorithms such as bcrypt
* or Argon2 should be used.
*/
public static function findDefaultHashAlgo() {
if (defined("PASSWORD_ARGON2ID"))
return PASSWORD_ARGON2ID;
if (defined("PASSWORD_ARGON2"))
return PASSWORD_ARGON2;
if (defined("PASSWORD_DEFAULT"))
return PASSWORD_DEFAULT;
if (defined("PASSWORD_BCRYPT"))
return PASSWORD_BCRYPT;
return false;
}
private static function isValidHashAlgo($algo): bool {
return (in_array($algo, password_algos()));
}
/*
* The password_hash() function not only uses a secure
* one-way hashing algorithm, but it automatically handles
* salt and prevents time based side-channel attacks.
*/
public static function do_password_hash(#[\SensitiveParameter] string $password): bool | string {
$pwdPeppered = self::makeHash($password);
$hashAlgo = Configure::get(
"security",
"hash_algo"
) ?? false;
if ($hashAlgo === false) {
throw new \Exception("Security Hash Algo not set!");
}
if (! self::isValidHashAlgo($hashAlgo)) {
throw new \Exception("Invalid Security Hash Alogo set");
}
return password_hash($pwdPeppered, $hashAlgo);
}
public static function doPasswordVerify(
#[\SensitiveParameter] string $inputPwd, #[\SensitiveParameter] $dbPassword
): bool {
$pwdPeppered = self::makeHash($inputPwd);
return password_verify($pwdPeppered, $dbPassword);
}
/**
* Make a secure Hash
* @param string $text to encode
* @param string $level (weak, low, high, max)
* @return string new Hashed
*/
public static function makeHash(#[\SensitiveParameter] string $text): string {
$level = Configure::get('security', 'hash_level');
if (empty($level)) {
$level = "normal";
}
$pepper = Configure::get('security', 'pepper_pwd');
if (strlen($pepper) < 12) {
throw new \Exception("Pepper Password, too short!");
}
$salt = Configure::get('security', 'salt_pwd');
if (strlen($salt) < 5) {
throw new \Exception("Salt Password, too short!");
}
switch (strtolower($level)) {
case 'max':
// Prefer computing using HMAC
if (function_exists("hash_hmac")) {
return hash_hmac("sha512", $text, $pepper);
}
// Sha512 hash is the next best thing
if (function_exists("hash")) {
return hash("sha512", $salt . $text . $pepper);
}
case 'normal':
// Prefer computing using HMAC
if (function_exists("hash_hmac")) {
return hash_hmac("sha256", $text, $pepper);
}
// Sha256 hash is the next best thing
if (function_exists("hash")) {
return hash("sha256", $salt . $text . $pepper);
}
case 'weak':
throw \Exception("Too weak of a Hash FN");
// return sha1($salt . $text . $pepper);
case 'low':
throw \Exception("Too weak of a Hash FN");
// return md5($salt . md5($text . $pepper));
default:
break;
}
return self::useHmac($level, $pepper);
}
/**
* @method filter_class
* @param type $class
* Please NEVER add a period or SLASH as it will allow BAD things!
* IT should be a-zA-Z0-9_ and that's it.
* @retval string of safe class name
*/
public static function filterClass(string $class): string {
if (Requires::isDangerous($class)) {
throw new \Exception("Dangerious URI!");
}
return preg_replace('/[^a-zA-Z0-9_]/', '', $class);
}
/**
* Filter possible unsafe URI, prevent ../up-level hackers
* @param string $uri
* @return string Safe URI
*/
public static function filterUri(string $uri): string {
if (Requires::isDangerous($uri) === true) {
throw new \Exception("Dangerious URI!");
}
return Requires::filterFileName($uri);
}
public static function idHash(): string {
return crc32($_SESSION['user_id']);
}
public static function isPrivateOrLocalIPSimple(string $ip): bool {
if (! self::getValidIp($ip)) {
return false; // Invalid
}
return (
$ip === '::1' || // IPv6 localhost
preg_match('/^127\./', $ip) || // IPv4 localhost
preg_match('/^10\./', $ip) || // 10.0.0.0/8
preg_match('/^172\.(1[6-9]|2[0-9]|3[0-1])\./', $ip) || // 172.16.0.0/12
preg_match('/^192\.168\./', $ip) || // 192.168.0.0/16
preg_match('/^fd[0-9a-f]{2}:/i', $ip) // IPv6 ULA (fc00::/7)
);
}
/**
* Filter IP return good IP or False!
* @param string $ip
* @return string | false
*/
public static function getValidIp(string $ip) {
return (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6));
}
public static function getValidPublicIp(string $ip) {
return (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_PRIV_RANGE));
}
/**
* Is the server on the local test domain name
* @return bool SERVER Domain name is on whitelist
*/
public static function isServerNameOnDomainList(array $whitelist): bool {
if (!isset($_SERVER['SERVER_NAME'])) {
return false;
}
return (in_array($_SERVER['SERVER_NAME'], $whitelist));
}
/**
* Check if same Domain as Server
* @return bool
*/
public static function requestIsSameDomain(): bool {
if (!isset($_SERVER['HTTP_REFERER'])) {
// No referer send, so can't be same domain!
return false;
} else {
$refererHost = parse_url($_SERVER['HTTP_REFERER'] . PHP_URL_HOST);
if ($refererHost === false) {
return false; // Malformed URL
}
$refed_host = $refererHost['host'] ?? "";
$server_host = $_SERVER['HTTP_HOST'];
return ($refed_host === $server_host);
}
}
public static function safeForEval(string $s): string {
//new line check
$nl = chr(10);
if (strpos($s, $nl)) {
throw new \Exception("String CR/LF not permitted");
}
$meta = ['$','{','}','[',']','`',';'];
$escaped = ['&#36','&#123','&#125','&#91','&#96','&#59'];
// add slashed for quotes and blackslashes
$out = addslashes($s);
// replace php meta chrs
$out = str_repeat($meta, $escaped, $out);
return $out;
}
public static function getClientIpAddress() {
$ipaddress = '';
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ipaddress = $_SERVER['HTTP_CLIENT_IP'];
} else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else if (isset($_SERVER['HTTP_X_FORWARDED'])) {
$ipaddress = $_SERVER['HTTP_X_FORWARDED'];
} else if (isset($_SERVER['HTTP_FORWARDED_FOR'])) {
$ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
} else if (isset($_SERVER['HTTP_FORWARDED'])) {
$ipaddress = $_SERVER['HTTP_FORWARDED'];
} else if (isset($_SERVER['REMOTE_ADDR'])) {
$ipaddress = $_SERVER['REMOTE_ADDR'];
} else {
$ipaddress = 'UNKNOWN';
}
return $ipaddress;
}
/**
* Make sure uploads (LIKE Images, etc...) do NOT run PHP code!!
* Checks for PHP tags inside of file.
* @param string $file
* @return bool true if PHP was found
*/
public static function fileContainsPhp(string $file): bool {
$file_handle = fopen($file, "r");
while (!feof($file_handle)) {
$line = fgets($file_handle);
$pos = strpos($line, '<?php');
if ($pos !== false) {
fclose($file_handle);
return true;
}
}
fclose($file_handle);
return false;
}
}