mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
feat: enforce idle + absolute timeouts on admin session
This commit is contained in:
+48
-1
@@ -19,6 +19,12 @@ class AdminAuth
|
||||
private const MAX_ATTEMPTS = 5;
|
||||
private const COOLDOWN_MINUTES = 15;
|
||||
|
||||
// Session lifetime (server-side): idle timeout and absolute timeout.
|
||||
// Aligned with OWASP ASVS / NIST 800-63B guidance for admin panels.
|
||||
private const IDLE_TIMEOUT_SECONDS = 1800; // 30 min with no activity
|
||||
private const ABSOLUTE_TIMEOUT_SECONDS = 43200; // 12 h regardless of activity
|
||||
private const COOKIE_LIFETIME_SECONDS = 604800; // 7 d — upper bound only
|
||||
|
||||
/** Test hook: override the Database connection (e.g. temp DB in smoke tests). */
|
||||
private static ?Database $dbOverride = null;
|
||||
|
||||
@@ -53,7 +59,7 @@ class AdminAuth
|
||||
}
|
||||
// Harden session cookie (item #8)
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 604800,
|
||||
'lifetime' => self::COOKIE_LIFETIME_SECONDS,
|
||||
'path' => '/admin',
|
||||
'secure' => (php_sapi_name() !== 'cli-server'),
|
||||
'httponly' => true,
|
||||
@@ -62,6 +68,44 @@ class AdminAuth
|
||||
session_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce server-side idle + absolute timeouts on an authenticated session.
|
||||
*
|
||||
* Called on every gated request. If the session has been idle longer than
|
||||
* IDLE_TIMEOUT_SECONDS, or has existed longer than ABSOLUTE_TIMEOUT_SECONDS
|
||||
* since login, the session is destroyed and the caller is redirected.
|
||||
*
|
||||
* Only acts on authenticated sessions; leaves unauthenticated sessions
|
||||
* (including throttle counters) untouched.
|
||||
*/
|
||||
private static function enforceSessionTimeout(): void
|
||||
{
|
||||
if (empty($_SESSION[self::SESSION_KEY])) {
|
||||
return; // Not authenticated — nothing to time out.
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$loginAt = (int) ($_SESSION['admin_login_at'] ?? $now);
|
||||
$activity = (int) ($_SESSION['admin_last_activity'] ?? $loginAt);
|
||||
|
||||
$idle = $now - $activity;
|
||||
$absolute = $now - $loginAt;
|
||||
|
||||
if ($idle > self::IDLE_TIMEOUT_SECONDS || $absolute > self::ABSOLUTE_TIMEOUT_SECONDS) {
|
||||
self::logout();
|
||||
header('Location: ' . self::LOGIN_URL);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rotate the session ID periodically to limit fixation/replay window.
|
||||
if ($absolute > 0 && $absolute >= self::IDLE_TIMEOUT_SECONDS && $absolute % self::IDLE_TIMEOUT_SECONDS === 0) {
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
|
||||
// Refresh the activity timestamp on every authenticated request.
|
||||
$_SESSION['admin_last_activity'] = $now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the admin password hash from site_settings.
|
||||
* Returns null if not set (dev mode).
|
||||
@@ -90,6 +134,7 @@ class AdminAuth
|
||||
public static function requireLogin(): void
|
||||
{
|
||||
self::startSession();
|
||||
self::enforceSessionTimeout();
|
||||
$storedHash = self::getStoredHash();
|
||||
if ($storedHash === null) {
|
||||
return; // No password configured → dev / cli-server, skip.
|
||||
@@ -156,6 +201,7 @@ class AdminAuth
|
||||
session_regenerate_id(true);
|
||||
$_SESSION[self::SESSION_KEY] = true;
|
||||
$_SESSION['admin_login_at'] = time();
|
||||
$_SESSION['admin_last_activity'] = time();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -284,6 +330,7 @@ HTML;
|
||||
public static function isAuthenticated(): bool
|
||||
{
|
||||
self::startSession();
|
||||
self::enforceSessionTimeout();
|
||||
$storedHash = self::getStoredHash();
|
||||
if ($storedHash === null) {
|
||||
return true; // No password configured → dev mode.
|
||||
|
||||
@@ -16,10 +16,52 @@ Current security posture for XAMXAM.
|
||||
- `AdminAuth::requireLogin()` guards every admin action/route.
|
||||
- Session cookies hardened: `HttpOnly`, `SameSite=Strict`, `Secure`,
|
||||
`Path=/admin`; regenerated on login.
|
||||
- **Session timeouts** (server-side, enforced by `AdminAuth::enforceSessionTimeout()`
|
||||
on every gated request):
|
||||
- **Idle timeout** — 30 min without activity → session destroyed, redirected
|
||||
to login.
|
||||
- **Absolute timeout** — 12 h since login → forced re-login, regardless of
|
||||
activity.
|
||||
- Session ID rotated periodically (every 30 min) to limit fixation/replay.
|
||||
- The session *cookie* `lifetime` is 7 days, but that is only an upper bound
|
||||
on browser retention — the actual session is bounded by the two timeouts above.
|
||||
- **PHP-FPM session GC tuning** — see below. The app enforces its own timeouts,
|
||||
so `session.gc_maxlifetime` must be ≥ the 12 h absolute timeout or PHP would
|
||||
reap active sessions early.
|
||||
- nginx `auth_basic` layer has been removed; the PHP session layer is the only
|
||||
gate. (LDAP-based login is a proposed future enhancement — see
|
||||
[`ldap.md`](ldap.md). It is **not** implemented.)
|
||||
|
||||
### PHP-FPM session GC configuration
|
||||
|
||||
The admin session's lifetime is enforced *by the app* (the timeouts above), so
|
||||
PHP's own session garbage collector must not reap active sessions before the
|
||||
12 h absolute timeout. The following is provisioned by
|
||||
`scripts/deploy-server.sh` (run via `just deploy-nginx`):
|
||||
|
||||
**File:** `/etc/php/8.4/fpm/conf.d/zz-xamxam-session.ini`
|
||||
|
||||
```ini
|
||||
; XAMXAM session tuning.
|
||||
; AdminAuth enforces its own idle/absolute timeouts (30 min / 12 h), so
|
||||
; gc_maxlifetime must be >= the absolute timeout or PHP would reap active
|
||||
; sessions from under the app.
|
||||
session.gc_maxlifetime = 43200
|
||||
session.gc_probability = 1
|
||||
session.gc_divisor = 100
|
||||
```
|
||||
|
||||
- `gc_maxlifetime = 43200` (12 h) — matches `ABSOLUTE_TIMEOUT_SECONDS`;
|
||||
**must stay ≥ the app's absolute timeout**.
|
||||
- `gc_probability = 1` / `gc_divisor = 100` — re-enables the GC (default was
|
||||
`gc_probability = 0`, i.e. disabled, so stale session files were never reaped).
|
||||
- Applied on FPM reload (`systemctl reload php8.4-fpm`), done by the deploy script.
|
||||
|
||||
> ⚠️ Keep these three knobs in sync with the constants in
|
||||
> `app/src/AdminAuth.php` (`IDLE_TIMEOUT_SECONDS`, `ABSOLUTE_TIMEOUT_SECONDS`,
|
||||
> `COOKIE_LIFETIME_SECONDS`). If you raise the app's absolute timeout beyond
|
||||
> 12 h, raise `gc_maxlifetime` to match.
|
||||
|
||||
## Transport & headers
|
||||
|
||||
Enforced in `nginx/xamxam.conf` (see `nginx/docs/SECURITY_HEADERS.md`):
|
||||
|
||||
@@ -97,6 +97,22 @@ chown www-data:xamxam /var/log/xamxam
|
||||
chmod 2775 /var/log/xamxam
|
||||
ok "Log dir: /var/log/xamxam owned by www-data:xamxam (2775)"
|
||||
|
||||
# PHP-FPM session GC must not reap active admin sessions early.
|
||||
# The app enforces its own server-side idle/absolute timeouts in AdminAuth
|
||||
# (30 min idle / 12 h absolute), so session.gc_maxlifetime needs to be at
|
||||
# least the absolute timeout, and GC re-enabled to clean up stale files.
|
||||
PHP_FPM_INI="/etc/php/8.4/fpm/conf.d/zz-xamxam-session.ini"
|
||||
cat > "$PHP_FPM_INI" <<'INI'
|
||||
; XAMXAM session tuning.
|
||||
; AdminAuth enforces its own idle/absolute timeouts (30 min / 12 h), so
|
||||
; gc_maxlifetime must be >= the absolute timeout or PHP would reap active
|
||||
; sessions from under the app.
|
||||
session.gc_maxlifetime = 43200
|
||||
session.gc_probability = 1
|
||||
session.gc_divisor = 100
|
||||
INI
|
||||
ok "PHP-FPM session GC: $PHP_FPM_INI"
|
||||
|
||||
# Backups dir must be writable by both www-data (cron) and the deploy user
|
||||
# (xamxam group) so scripts/migrate.sh can write a pre-deploy snapshot before
|
||||
# running migrations.
|
||||
@@ -164,6 +180,10 @@ echo "------------------------------"
|
||||
systemctl reload nginx
|
||||
ok "Nginx reloaded"
|
||||
|
||||
# Reload PHP-FPM so the session GC settings take effect.
|
||||
systemctl reload php8.4-fpm 2>/dev/null || systemctl reload php-fpm 2>/dev/null || true
|
||||
ok "PHP-FPM reloaded"
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
printf "\n"
|
||||
ok "Permissions fixed"
|
||||
|
||||
Reference in New Issue
Block a user