2e450dc01d
P1-A: Fix CSP - add fonts.googleapis.com to style-src, fonts.gstatic.com to font-src
P1-B: CSRF token rotation - add rotateToken() to CsrfMiddleware; bootstrap.php rotates
after successful validation and stores in $GLOBALS['_new_csrf_token']; add
apiRespond() helper to append token to responses; lt.api interceptor in
layout_footer.php auto-updates window.CSRF_TOKEN from responses
P1-C: Styled 403/404 error views with TDS layout instead of raw text; index.php now
uses requireAdmin() helper eliminating 7 duplicated guard blocks (P3-D)
P2-A: Remove duplicate JS-generated keyboard help modal from keyboard-shortcuts.js;
'?' key now routes to static #lt-keys-help modal in footer
P2-B: Asset versioning driven by config ASSET_VERSION key; base.css and base.js get
?v= cache-busting in layout_header.php
P2-C: Add data-theme="dark" to <html> tag to prevent FOUC on light-mode users
P2-E: Escape status value in dashboard.js hover preview class attribute via lt.escHtml()
P2-F: Replace bespoke showLoadingOverlay() with lt-spinner / lt-loading-text from
base.css; add .lt-loading-overlay wrapper CSS to dashboard.css
P2-G: Add keyboard-shortcuts.js to all 7 admin views so J/K nav and ? help work
P3-A: APP_NAME, APP_SUBTITLE, APP_VERSION driven from config.php; layout header/footer
use config values instead of hardcoded strings
P3-G: Replace custom initTableSorting() with lt.sortTable.init() which manages aria-sort
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
67 lines
2.0 KiB
PHP
67 lines
2.0 KiB
PHP
<?php
|
|
/**
|
|
* CSRF Protection Middleware
|
|
* Generates and validates CSRF tokens for all state-changing operations
|
|
*/
|
|
class CsrfMiddleware {
|
|
private static string $tokenName = 'csrf_token';
|
|
private static string $tokenTime = 'csrf_token_time';
|
|
private static int $tokenLifetime = 3600; // 1 hour
|
|
|
|
/**
|
|
* Generate a new CSRF token
|
|
*/
|
|
public static function generateToken(): string {
|
|
$_SESSION[self::$tokenName] = bin2hex(random_bytes(32));
|
|
$_SESSION[self::$tokenTime] = time();
|
|
return $_SESSION[self::$tokenName];
|
|
}
|
|
|
|
/**
|
|
* Get current CSRF token, regenerate if expired
|
|
*/
|
|
public static function getToken(): string {
|
|
if (!isset($_SESSION[self::$tokenName]) || self::isTokenExpired()) {
|
|
return self::generateToken();
|
|
}
|
|
return $_SESSION[self::$tokenName];
|
|
}
|
|
|
|
/**
|
|
* Validate CSRF token (constant-time comparison)
|
|
*/
|
|
public static function validateToken(string $token): bool {
|
|
if (!isset($_SESSION[self::$tokenName])) {
|
|
return false;
|
|
}
|
|
|
|
if (self::isTokenExpired()) {
|
|
self::generateToken(); // Auto-regenerate expired token
|
|
return false;
|
|
}
|
|
|
|
// Constant-time comparison to prevent timing attacks
|
|
return hash_equals($_SESSION[self::$tokenName], $token);
|
|
}
|
|
|
|
/**
|
|
* Rotate the CSRF token after a successful validated POST.
|
|
* Call this after validateToken() returns true, then include
|
|
* the new token in the JSON response as 'csrf_token' so the
|
|
* client can update window.CSRF_TOKEN for subsequent requests.
|
|
*
|
|
* @return string The new token
|
|
*/
|
|
public static function rotateToken(): string {
|
|
return self::generateToken();
|
|
}
|
|
|
|
/**
|
|
* Check if token is expired
|
|
*/
|
|
private static function isTokenExpired(): bool {
|
|
return !isset($_SESSION[self::$tokenTime]) ||
|
|
(time() - $_SESSION[self::$tokenTime]) > self::$tokenLifetime;
|
|
}
|
|
}
|