// // Usage on a POST handler (first line, before reading $_POST): // csrf_check(); // // The token is one-per-session (Delight\Auth's session). It rotates on // logout because the PHP session is destroyed. State-changing GETs are // not protected (and shouldn't exist — see logout.php for the POST flip). declare(strict_types=1); function csrf_token(): string { if (session_status() !== PHP_SESSION_ACTIVE) { // Defensive: bootstrap.php always starts the session, but if a // caller forgot, do it here so the token can be stored somewhere. session_start(); } if (empty($_SESSION['csrf_token']) || !is_string($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } /** * Render a hidden form input carrying the current session's CSRF token. * Use this inside every POST
in the dev portal. */ function csrf_field(): string { $tok = htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8'); return ''; } /** * Verify the incoming POST carries a matching CSRF token. Dies with a * 403 on mismatch. Accepts the token from either $_POST['csrf_token'] * or the X-CSRF-Token header (for fetch() callers). */ function csrf_check(): void { if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } $expected = $_SESSION['csrf_token'] ?? ''; $provided = $_POST['csrf_token'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''); if (!is_string($expected) || $expected === '' || !is_string($provided) || $provided === '' || !hash_equals($expected, $provided)) { http_response_code(403); header('Content-Type: text/plain; charset=utf-8'); echo "CSRF check failed. Refresh the page and try again."; exit; } }