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.
312 lines
10 KiB
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 = ['$','{','}','[','`',';'];
|
|
// 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;
|
|
}
|
|
}
|
|
|