Files
T
2026-09-16 23:20:08 +07:00

153 lines
4.2 KiB
PHP

<?php
namespace App\Helpers;
/**
* Class Security
* Enterprise Security Utilities (JWT Encoder/Decoder, XSS Sanitizer, CSRF, CSP)
*
* @package App\Helpers
*/
class Security
{
/**
* Sanitize String against XSS (Cross-Site Scripting)
*
* @param string|null $input
* @return string
*/
public static function clean(?string $input): string
{
if ($input === null) return '';
return htmlspecialchars(trim($input), ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* Recursive XSS Sanitize for Array or Object
*
* @param mixed $data
* @return mixed
*/
public static function cleanArray($data)
{
if (is_array($data)) {
return array_map([self::class, 'cleanArray'], $data);
}
if (is_string($data)) {
return self::clean($data);
}
return $data;
}
/**
* Generate CSRF Token and store in Session
*
* @return string
*/
public static function generateCsrfToken(): string
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
/**
* Verify CSRF Token
*
* @param string|null $token
* @return bool
*/
public static function verifyCsrfToken(?string $token): bool
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (empty($_SESSION['csrf_token']) || empty($token)) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Generate JSON Web Token (JWT RFC7519)
*
* @param array $payload
* @param string $secret
* @param int $ttl Seconds
* @return string
*/
public static function generateJwt(array $payload, string $secret, int $ttl = 3600): string
{
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
$payload['iat'] = time();
$payload['exp'] = time() + $ttl;
$payload['iss'] = 'https://ttmqms.hospital.local';
$base64UrlHeader = self::base64UrlEncode($header);
$base64UrlPayload = self::base64UrlEncode(json_encode($payload));
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
$base64UrlSignature = self::base64UrlEncode($signature);
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
}
/**
* Verify and Decode JWT Token
*
* @param string $token
* @param string $secret
* @return array|null Payload array if valid, null if invalid or expired
*/
public static function decodeJwt(string $token, string $secret): ?array
{
$parts = explode('.', $token);
if (count($parts) !== 3) return null;
[$header64, $payload64, $sig64] = $parts;
$signature = self::base64UrlDecode($sig64);
$expectedSig = hash_hmac('sha256', $header64 . "." . $payload64, $secret, true);
if (!hash_equals($expectedSig, $signature)) {
return null; // Signature mismatch (Tampered)
}
$payload = json_decode(self::base64UrlDecode($payload64), true);
if (!$payload || !isset($payload['exp']) || $payload['exp'] < time()) {
return null; // Token expired
}
return $payload;
}
private static function base64UrlEncode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
private static function base64UrlDecode(string $data): string
{
return base64_decode(strtr($data, '-_', '+/'));
}
/**
* Get Client IP Address (Supporting Proxy X-Forwarded-For)
*/
public static function getClientIp(): string
{
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
return $_SERVER['HTTP_CLIENT_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($ips[0]);
}
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
}