mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 15:21:22 +02:00
Reintroduce TFE duration metadata: DB columns, form fields, controllers, views, and migration
Add 'unsafe-eval' to CSP script-src directives (htmx requires Function())
This commit is contained in:
+46
-19
@@ -3,9 +3,7 @@
|
||||
/**
|
||||
* Minimal PHP session guard for the admin panel.
|
||||
*
|
||||
* This is a defence-in-depth layer that sits behind nginx Basic Auth.
|
||||
* It protects against proxy misconfiguration, bypass, and local-dev
|
||||
* scenarios where the reverse proxy may be absent.
|
||||
* Password-only authentication via an HTML login form.
|
||||
*
|
||||
* The admin password hash is stored in the site_settings table
|
||||
* (key = 'admin_password_hash').
|
||||
@@ -17,6 +15,10 @@ class AdminAuth
|
||||
private const SESSION_KEY = 'admin_authenticated';
|
||||
private const LOGIN_URL = '/admin/login.php';
|
||||
|
||||
// Throttle: max 5 attempts before mandatory delay, cooldown 15 min.
|
||||
private const MAX_ATTEMPTS = 5;
|
||||
private const COOLDOWN_MINUTES = 15;
|
||||
|
||||
/**
|
||||
* Start the PHP session with hardened cookie parameters.
|
||||
* Idempotent — safe to call even if session is already active.
|
||||
@@ -61,10 +63,7 @@ class AdminAuth
|
||||
* Authentication order:
|
||||
* 1. No password hash configured → dev mode, pass through.
|
||||
* 2. Session already authenticated → pass through.
|
||||
* 3. nginx Basic Auth password present in $_SERVER['PHP_AUTH_PW']
|
||||
* → validate it with password_verify; on success create session
|
||||
* (seamless: user only sees the browser Basic Auth dialog).
|
||||
* 4. Neither → redirect to the PHP login form.
|
||||
* 3. Neither → redirect to the PHP login form.
|
||||
*/
|
||||
public static function requireLogin(): void
|
||||
{
|
||||
@@ -76,11 +75,6 @@ class AdminAuth
|
||||
if (!empty($_SESSION[self::SESSION_KEY])) {
|
||||
return; // Already authenticated via session.
|
||||
}
|
||||
// Try to auto-authenticate from the nginx Basic Auth credentials.
|
||||
if (isset($_SERVER['PHP_AUTH_PW']) && self::verifyHash($_SERVER['PHP_AUTH_PW'], $storedHash)) {
|
||||
$_SESSION[self::SESSION_KEY] = true;
|
||||
return;
|
||||
}
|
||||
header('Location: ' . self::LOGIN_URL);
|
||||
exit;
|
||||
}
|
||||
@@ -89,15 +83,54 @@ class AdminAuth
|
||||
* Validate a plaintext password against the stored hash.
|
||||
* On success: regenerates the session ID and marks the session authenticated.
|
||||
*
|
||||
* Throttling: after MAX_ATTEMPTS consecutive failures, a mandatory delay is
|
||||
* enforced (incremental: 1s, 2s, 4s, … up to 60s). Returns the same `false`
|
||||
* result as a wrong password so the attacker cannot distinguish the reason.
|
||||
*
|
||||
* @return bool true on success, false on wrong password / no hash stored.
|
||||
*/
|
||||
public static function login(string $password): bool
|
||||
{
|
||||
$storedHash = self::getStoredHash();
|
||||
if ($storedHash === null || !self::verifyHash($password, $storedHash)) {
|
||||
if ($storedHash === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self::startSession();
|
||||
$alreadyAuthed = !empty($_SESSION[self::SESSION_KEY]);
|
||||
|
||||
// ── Throttle: only on unauthenticated login attempts ────────────────
|
||||
if (!$alreadyAuthed) {
|
||||
$attempts = (int) ($_SESSION['auth_attempts'] ?? 0);
|
||||
$firstAt = (int) ($_SESSION['auth_first_attempt'] ?? 0);
|
||||
$now = time();
|
||||
|
||||
// Cooldown window — reset after COOLDOWN_MINUTES
|
||||
if ($attempts > 0 && ($now - $firstAt) > self::COOLDOWN_MINUTES * 60) {
|
||||
$attempts = 0;
|
||||
$firstAt = 0;
|
||||
unset($_SESSION['auth_attempts'], $_SESSION['auth_first_attempt']);
|
||||
}
|
||||
|
||||
if ($attempts >= self::MAX_ATTEMPTS) {
|
||||
$extra = $attempts - self::MAX_ATTEMPTS;
|
||||
$delay = min(1 << min($extra, 6), 60); // 1s → 2s → 4s … → 60s cap
|
||||
sleep($delay);
|
||||
}
|
||||
}
|
||||
|
||||
if (!self::verifyHash($password, $storedHash)) {
|
||||
if (!$alreadyAuthed) {
|
||||
if ($attempts === 0) {
|
||||
$_SESSION['auth_first_attempt'] = $now;
|
||||
}
|
||||
$_SESSION['auth_attempts'] = $attempts + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Success: clear throttling, create/refresh session ──────────────
|
||||
unset($_SESSION['auth_attempts'], $_SESSION['auth_first_attempt']);
|
||||
session_regenerate_id(true);
|
||||
$_SESSION[self::SESSION_KEY] = true;
|
||||
$_SESSION['admin_login_at'] = time();
|
||||
@@ -145,12 +178,6 @@ class AdminAuth
|
||||
if (!empty($_SESSION[self::SESSION_KEY])) {
|
||||
return true;
|
||||
}
|
||||
// Also accept nginx Basic Auth credentials directly (e.g. HTMX fragment
|
||||
// requests that arrive before a PHP session has been established).
|
||||
if (isset($_SERVER['PHP_AUTH_PW']) && self::verifyHash($_SERVER['PHP_AUTH_PW'], $storedHash)) {
|
||||
$_SESSION[self::SESSION_KEY] = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user