security: add PHP session auth guard for admin panel (item #2, CRITICAL)

- lib/AdminAuth.php: new class with requireLogin(), login(), logout(),
  isAuthenticated(); starts session with hardened cookie params
  (HttpOnly, SameSite=Strict, Secure, Path=/admin) — also resolves
  item #8 (session cookie hardening)
- requireLogin() auto-authenticates from nginx Basic Auth credentials
  ($_SERVER['PHP_AUTH_PW']) so the user only sees one browser prompt;
  falls back to /admin/login.php if the proxy is absent/misconfigured
- config/admin_credentials.php: gitignored credential store; define
  ADMIN_PASSWORD_HASH with a bcrypt hash to enable PHP auth
- config/admin_credentials.example.php: template for the above
- config/bootstrap.php: auto-loads admin_credentials.php if present
- .gitignore: exclude config/admin_credentials.php
- public/admin/login.php: fallback login form (shown only when nginx
  Basic Auth is bypassed / proxy absent)
- public/admin/logout.php: session destruction + redirect to login
- All 7 admin PHP files: replace session_start() with
  AdminAuth::requireLogin() (defence-in-depth behind nginx Basic Auth)
- public/admin/inc/head.php: Déconnexion button when ADMIN_PASSWORD_HASH
  is defined
- nginx/PHP_AUTH_LAYER.md: documents dual-auth architecture, UX flow,
  and setup instructions
- docs/TODO.SECURITY.md: items #2 and #8 moved to Resolved; priority
  order updated (all CRITICAL done)
This commit is contained in:
Théophile Gervreau-Mercier
2026-02-08 14:12:32 +01:00
parent a2b1ff5f41
commit 8613f71112
17 changed files with 369 additions and 25 deletions

View File

@@ -57,12 +57,15 @@ Reusable HTML components:
## Security
- All pages require HTTP Basic Auth (configured in nginx)
- All pages require HTTP Basic Auth (configured in nginx) — primary layer
- All pages require PHP session auth (`AdminAuth::requireLogin()`) — defence-in-depth
- CSRF tokens protect all forms
- File uploads validated and sanitized
- Database queries use prepared statements
- Upload directory outside public/ in production
See `nginx/PHP_AUTH_LAYER.md` for details on the dual-auth architecture.
## Templates
The `inc/` folder contains shared templates:
@@ -96,7 +99,8 @@ Backend actions (not directly accessed):
```php
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
session_start();
require_once __DIR__ . '/../../lib/AdminAuth.php';
AdminAuth::requireLogin();
$pageTitle = "Your Page Title";
?>
<?php include "inc/head.php" ?>
@@ -114,7 +118,8 @@ $pageTitle = "Your Page Title";
```php
<?php
require_once __DIR__ . "/../../config/bootstrap.php";
session_start();
require_once __DIR__ . '/../../lib/AdminAuth.php';
AdminAuth::requireLogin();
// Verify CSRF token
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {

View File

@@ -1,15 +1,15 @@
<?php // formulaire.php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../lib/AdminAuth.php';
// Configure error reporting
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', 'error.log');
// Start session for CSRF protection
session_start();
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Verify CSRF token
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) ||

View File

@@ -1,11 +1,13 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../lib/AdminAuth.php';
/**
* Handle publish/unpublish actions for theses
*/
session_start();
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
require_once __DIR__ . '/../../lib/Database.php';

View File

@@ -1,9 +1,11 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../lib/AdminAuth.php';
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Start session and generate CSRF token
session_start();
if (empty($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}

View File

@@ -1,9 +1,10 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../lib/AdminAuth.php';
// Edit thesis page
session_start();
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {

View File

@@ -1,11 +1,13 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../lib/AdminAuth.php';
// CSV Import page for Post-ERG thesis database
// This page allows importing thesis data from CSV files
session_start();
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {

View File

@@ -62,5 +62,8 @@
echo implode(' ', $navLinks);
?>
<?php if (defined('ADMIN_PASSWORD_HASH')): ?>
<a href="/admin/logout.php"><button>🔐 Déconnexion</button></a>
<?php endif; ?>
</nav>
</header>

View File

@@ -1,9 +1,10 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../lib/AdminAuth.php';
// List all theses in the database
session_start();
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {

60
public/admin/login.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
require_once __DIR__ . '/../../config/bootstrap.php';
require_once __DIR__ . '/../../lib/AdminAuth.php';
// If no password is configured, nothing to log into — go straight to admin.
if (!defined('ADMIN_PASSWORD_HASH')) {
header('Location: /admin/');
exit;
}
// Already authenticated — redirect to admin.
if (AdminAuth::isAuthenticated()) {
header('Location: /admin/');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$password = $_POST['password'] ?? '';
if (AdminAuth::login($password)) {
header('Location: /admin/');
exit;
}
// Intentionally vague error — avoid user-enumeration.
$error = 'Mot de passe incorrect.';
}
$pageTitle = 'Connexion';
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($pageTitle); ?> — Post-ERG Admin</title>
<link rel="stylesheet" href="/assets/modern-normalize.min.css">
<link rel="stylesheet" href="/assets/admin.css">
<link rel="shortcut icon" href="/assets/admin_favicon.svg" type="image/svg+xml">
</head>
<body>
<header>
<h1><?php echo htmlspecialchars($pageTitle); ?></h1>
</header>
<main>
<?php if ($error): ?>
<div class="alert-error">
<strong>⚠️ <?php echo htmlspecialchars($error); ?></strong>
</div>
<?php endif; ?>
<form method="post" action="/admin/login.php">
<fieldset>
<legend>Authentification admin</legend>
<label for="password">Mot de passe</label>
<input type="password" id="password" name="password" required autofocus>
<button type="submit">Se connecter</button>
</fieldset>
</form>
</main>
</body>
</html>

8
public/admin/logout.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
require_once __DIR__ . '/../../config/bootstrap.php';
require_once __DIR__ . '/../../lib/AdminAuth.php';
AdminAuth::logout();
header('Location: /admin/login.php');
exit;

View File

@@ -1,6 +1,10 @@
<?php
// Bootstrap application
require_once __DIR__ . "/../../config/bootstrap.php";
require_once __DIR__ . '/../../lib/AdminAuth.php';
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
AdminAuth::requireLogin();
// Configure error reporting
ini_set('display_errors', 0);