<?php declare(strict_types=1); if (!defined('DASHBOARD_ROOT')) { http_response_code(403); exit('Direct access denied.'); } class CSRF { private const TOKEN_KEY = '_csrf_token'; private const TOKEN_TIME_KEY = '_csrf_generated'; private const TOKEN_LIFETIME = 3600; public static function token(): string { if ( !isset($_SESSION[self::TOKEN_KEY]) || !isset($_SESSION[self::TOKEN_TIME_KEY]) || (time() - $_SESSION[self::TOKEN_TIME_KEY]) > self::TOKEN_LIFETIME ) { self::regenerate(); } return $_SESSION[self::TOKEN_KEY]; } public static function regenerate(): string { $token = bin2hex(random_bytes(32)); $_SESSION[self::TOKEN_KEY] = $token; $_SESSION[self::TOKEN_TIME_KEY] = time(); return $token; } public static function validate(?string $submitted): bool { if ($submitted === null || !isset($_SESSION[self::TOKEN_KEY])) { return false; } $valid = hash_equals($_SESSION[self::TOKEN_KEY], $submitted); if ($valid && !self::isAjax()) { self::regenerate(); } return $valid; } private static function isAjax(): bool { if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') { return true; } if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) { return true; } if (isset($_SERVER['HTTP_SEC_FETCH_MODE']) && $_SERVER['HTTP_SEC_FETCH_MODE'] === 'cors') { return true; } if (isset($_SERVER['HTTP_SEC_FETCH_DEST']) && $_SERVER['HTTP_SEC_FETCH_DEST'] === 'empty') { return true; } return false; } public static function enforce(): void { $submitted = $_POST['_csrf'] ?? $_POST['csrf_token'] ?? $_GET['_csrf'] ?? $_GET['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null; if (!self::validate($submitted)) { Events::record(Events::CAT_SECURITY, 'csrf.rejected', 'request', null, [ 'method' => $_SERVER['REQUEST_METHOD'], 'uri' => $_SERVER['REQUEST_URI'], ]); http_response_code(403); exit(json_encode(['success' => false, 'error' => 'Invalid security token.'])); } } public static function field(): string { return '<input type="hidden" name="_csrf" value="' . htmlspecialchars(self::token()) . '">'; } public static function headerValue(): string { return self::token(); } } 