fix: block doc artifacts and add error templates

This commit is contained in:
2026-07-27 01:55:30 +04:00
parent d81dd71a46
commit 71e367b86f
7 changed files with 422 additions and 16 deletions
+5
View File
@@ -8,6 +8,8 @@ robots.txt
site.webmanifest site.webmanifest
mentions-legales.php mentions-legales.php
dons.php dons.php
404.php
500.php
# Fichiers de l'IDE/éditeur # Fichiers de l'IDE/éditeur
.vscode/ .vscode/
@@ -37,6 +39,9 @@ uploads/*
# vendor/ # vendor/
# node_modules/ # node_modules/
# Rapport d'audit interne OKI (ne pas committer)
audit/
# Artefacts de tests locaux # Artefacts de tests locaux
.pytest_cache/ .pytest_cache/
__pycache__/ __pycache__/
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* Page d'erreur 404 - Modèle pour ANNU KUTE CED
*
* Pour l'activer : copiez ce fichier vers 404.php
*
* Utilisée automatiquement par dons.php lorsque les dons sont désactivés.
* Peut aussi servir de page d'erreur globale, par exemple :
* - Apache : ErrorDocument 404 /404.php
* - nginx : error_page 404 /404.php;
*/
// Chargement autonome si la page est appelée directement
// (dons.php a déjà chargé ces fichiers avant d'inclure celui-ci)
if (!defined('SITE_NAME')) {
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/security.php';
}
http_response_code(404);
// Incluse depuis dons.php, la page est servie avant setSecurityHeaders() :
// on applique les en-têtes de sécurité ici (aucune sortie n'a encore eu lieu).
if (function_exists('setSecurityHeaders')) {
setSecurityHeaders();
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page introuvable - <?php echo SITE_NAME; ?></title>
<meta name="description" content="La page demandée est introuvable ou n'est pas disponible.">
<meta name="robots" content="noindex">
<!-- Styles -->
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime(__DIR__ . '/css/styles.css'); ?>">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" crossorigin="anonymous">
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
<meta name="theme-color" content="#FF0000">
<!-- Script pour éviter le flash en mode sombre -->
<script nonce="<?php echo getCspNonce(); ?>">
(function() {
const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const shouldUseDark = savedTheme === 'dark' || (!savedTheme && systemPrefersDark);
if (shouldUseDark) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<?php include __DIR__ . '/includes/sidebar.php'; ?>
<main class="main-content">
<?php include __DIR__ . '/includes/header.php'; ?>
<div class="container">
<section class="error-page">
<p class="error-code" aria-hidden="true">404</p>
<h1>Page introuvable</h1>
<p>La page que vous recherchez n'existe pas, a été déplacée ou n'est pas disponible pour le moment.</p>
<p class="error-actions">
<a href="index.php" class="error-home-link"><i class="fas fa-home" aria-hidden="true"></i> Retour à l'accueil</a>
</p>
</section>
</div>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
<?php include __DIR__ . '/includes/mobile-menu.php'; ?>
<script src="js/main.js"></script>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* Page d'erreur 500 - Modèle pour ANNU KUTE CED
*
* Pour l'activer : copiez ce fichier vers 500.php
*
* Utilisée automatiquement par dons.php lorsqu'aucune plateforme de don
* n'est configurée. Peut aussi servir de page d'erreur globale, par exemple :
* - Apache : ErrorDocument 500 /500.php
* - nginx : error_page 500 /500.php;
*
* N'affichez jamais le détail de l'erreur (trace, chemin serveur…) sur
* cette page : il est consigné dans le journal d'erreurs du serveur.
*/
// Chargement autonome si la page est appelée directement
// (dons.php a déjà chargé ces fichiers avant d'inclure celui-ci)
if (!defined('SITE_NAME')) {
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/security.php';
}
http_response_code(500);
// Incluse depuis dons.php, la page est servie avant setSecurityHeaders() :
// on applique les en-têtes de sécurité ici (aucune sortie n'a encore eu lieu).
if (function_exists('setSecurityHeaders')) {
setSecurityHeaders();
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Erreur interne - <?php echo SITE_NAME; ?></title>
<meta name="description" content="Une erreur interne est survenue. Veuillez réessayer plus tard.">
<meta name="robots" content="noindex">
<!-- Styles -->
<link rel="stylesheet" href="css/styles.css?v=<?php echo filemtime(__DIR__ . '/css/styles.css'); ?>">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" crossorigin="anonymous">
<!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<link rel="icon" type="image/x-icon" href="img/favicon.ico">
<meta name="theme-color" content="#FF0000">
<!-- Script pour éviter le flash en mode sombre -->
<script nonce="<?php echo getCspNonce(); ?>">
(function() {
const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const shouldUseDark = savedTheme === 'dark' || (!savedTheme && systemPrefersDark);
if (shouldUseDark) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<?php include __DIR__ . '/includes/sidebar.php'; ?>
<main class="main-content">
<?php include __DIR__ . '/includes/header.php'; ?>
<div class="container">
<section class="error-page">
<p class="error-code" aria-hidden="true">500</p>
<h1>Une erreur est survenue</h1>
<p>Le site rencontre un problème temporaire. L'incident a été consigné
et sera examiné par l'équipe technique. Merci de réessayer dans quelques instants.</p>
<p class="error-actions">
<a href="index.php" class="error-home-link"><i class="fas fa-home" aria-hidden="true"></i> Retour à l'accueil</a>
</p>
</section>
</div>
</main>
<?php include __DIR__ . '/includes/footer.php'; ?>
<?php include __DIR__ . '/includes/mobile-menu.php'; ?>
<script src="js/main.js"></script>
</body>
</html>
+7
View File
@@ -22,6 +22,13 @@ RewriteRule ^(includes|cache|docs|conf)/ - [F,L]
Require all denied Require all denied
</FilesMatch> </FilesMatch>
# Bloquer les artefacts de documentation à la racine web
# (DEPLOY.adoc/html/pdf, README.adoc…) : ils divulguent le plan
# d'infrastructure et ne doivent jamais être servis s'ils sont copiés
# sur l'hébergement. [^/]+ = fichiers à la racine uniquement, les
# éventuels .html légitimes en sous-répertoire restent servis.
RewriteRule ^[^/]+\.(adoc|html|pdf)$ - [F,L,NC]
# Empêcher l'exploration des répertoires # Empêcher l'exploration des répertoires
Options -Indexes Options -Indexes
+10
View File
@@ -40,6 +40,16 @@ server {
return 404; return 404;
} }
# Bloquer les artefacts de documentation à la racine web
# (DEPLOY.adoc/html/pdf, README.adoc…) : ils divulguent le plan
# d'infrastructure et ne doivent jamais être servis s'ils sont copiés
# sur l'hébergement. [^/]+ = fichiers à la racine uniquement, les
# éventuels .html légitimes en sous-répertoire restent servis.
location ~* ^/[^/]+\.(adoc|html|pdf)$ {
deny all;
return 404;
}
# Bloquer l'accès aux fichiers cachés # Bloquer l'accès aux fichiers cachés
location ~ /\. { location ~ /\. {
deny all; deny all;
+18 -16
View File
@@ -123,7 +123,7 @@ function linkUrlsInText(string $text): string {
?> ?>
<!-- Script pour éviter le flash en mode sombre --> <!-- Script pour éviter le flash en mode sombre -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
(function() { (function() {
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
@@ -223,10 +223,10 @@ function linkUrlsInText(string $text): string {
<!-- Onglets don ponctuel / mensuel --> <!-- Onglets don ponctuel / mensuel -->
<div class="donation-tabs"> <div class="donation-tabs">
<button class="tab-btn active" onclick="switchTab('onetime')"> <button class="tab-btn active" data-tab="onetime">
<i class="fas fa-hand-holding-heart"></i> Don ponctuel <i class="fas fa-hand-holding-heart"></i> Don ponctuel
</button> </button>
<button class="tab-btn" onclick="switchTab('monthly')"> <button class="tab-btn" data-tab="monthly">
<i class="fas fa-sync-alt"></i> Don mensuel <i class="fas fa-sync-alt"></i> Don mensuel
</button> </button>
</div> </div>
@@ -308,20 +308,22 @@ function linkUrlsInText(string $text): string {
<?php include 'includes/mobile-menu.php'; ?> <?php include 'includes/mobile-menu.php'; ?>
<!-- Script pour les onglets --> <!-- Script pour les onglets -->
<script> <script nonce="<?php echo getCspNonce(); ?>">
function switchTab(tab) { document.querySelectorAll('.tab-btn').forEach(btn => {
// Cacher tous les contenus et désactiver tous les boutons btn.addEventListener('click', function() {
document.querySelectorAll('.tab-content').forEach(content => { // Cacher tous les contenus et désactiver tous les boutons
content.classList.remove('active'); document.querySelectorAll('.tab-content').forEach(content => {
}); content.classList.remove('active');
document.querySelectorAll('.tab-btn').forEach(btn => { });
btn.classList.remove('active'); document.querySelectorAll('.tab-btn').forEach(otherBtn => {
}); otherBtn.classList.remove('active');
});
// Afficher le contenu sélectionné et activer le bouton // Afficher le contenu sélectionné et activer le bouton
document.getElementById(tab + '-tab').classList.add('active'); document.getElementById(this.dataset.tab + '-tab').classList.add('active');
event.target.classList.add('active'); this.classList.add('active');
} });
});
</script> </script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
+208
View File
@@ -0,0 +1,208 @@
<?php
/**
* Tests du groupe « dons » : synchronisation dons.php / dons.php.sample,
* gabarits d'erreur 404/500 et blocage des artefacts de documentation.
*
* ARC-7 : dons.php inclut 404.php / 500.php — les gabarits .sample existent
* et le chemin d'erreur affiche une page propre (sans warning PHP).
* SEC-6 : DEPLOY.adoc/html/pdf à la racine sont bloqués par les modèles
* nginx / Apache.
* Dérive : linkUrlsInText() présent dans dons.php comme dans le .sample.
*/
$root = dirname(__DIR__, 2);
// --- Synchronisation dons.php / dons.php.sample -----------------------------
$donsLive = file_get_contents($root . '/dons.php');
$donsSample = file_get_contents($root . '/dons.php.sample');
assertContains(
'function linkUrlsInText',
$donsLive,
'dons.php définit linkUrlsInText() comme le .sample'
);
assertContains(
'linkUrlsInText(DONATIONS_OKI_DISCLAIMER)',
$donsLive,
'dons.php rend le message de transparence avec linkUrlsInText()'
);
assertNotContains(
'htmlspecialchars(DONATIONS_OKI_DISCLAIMER)',
$donsLive,
'dons.php n\'utilise plus un simple htmlspecialchars() pour le message de transparence'
);
// La définition de la fonction doit être identique dans les deux fichiers
// (verrou anti-dérive entre le modèle et la copie d'instance)
$extractFunction = function (string $source): ?string {
if (preg_match('/(function linkUrlsInText\(string \$text\): string \{.*?\n\})/s', $source, $m)) {
return $m[1];
}
return null;
};
assertEquals(
$extractFunction($donsSample),
$extractFunction($donsLive),
'linkUrlsInText() est identique dans dons.php et dons.php.sample'
);
// --- linkUrlsInText() : comportement ----------------------------------------
if (!function_exists('linkUrlsInText')) {
eval($extractFunction($donsSample));
}
assertEquals(
'Texte &amp; &quot;échappé&quot;',
linkUrlsInText('Texte & "échappé"'),
'linkUrlsInText échappe le texte sans URL'
);
$linked = linkUrlsInText('Soutenez-nous sur https://example.com/dons merci');
assertContains(
'<a href="https://example.com/dons" target="_blank" rel="noopener noreferrer">https://example.com/dons</a>',
$linked,
'linkUrlsInText rend les URLs cliquables avec target et rel'
);
assertEquals(
'&lt;script&gt;alert(1)&lt;/script&gt;',
linkUrlsInText('<script>alert(1)</script>'),
'linkUrlsInText échappe le HTML injecté dans le texte'
);
assertContains(
'href="https://example.com/?a=1&amp;b=2"',
linkUrlsInText('Lien : https://example.com/?a=1&b=2'),
'linkUrlsInText échappe les & dans l\'URL du lien'
);
// --- Gabarits d'erreur 404.php.sample / 500.php.sample (ARC-7) ---------------
foreach (['404' => '404.php.sample', '500' => '500.php.sample'] as $code => $file) {
$path = $root . '/' . $file;
assertTrue(file_exists($path), "$file existe");
$template = file_get_contents($path);
assertContains(
"http_response_code($code)",
$template,
"$file positionne le code HTTP $code"
);
assertContains(
'noindex',
$template,
"$file demande la non-indexation (robots noindex)"
);
assertNotContains(
'style="',
$template,
"$file n'utilise pas d'attribut style inline (bloqué par la CSP)"
);
// Tout <script> inline (sans src) doit porter un nonce CSP
preg_match_all('/<script(?![^>]*\bsrc=)[^>]*>/i', $template, $inlineScripts);
$allNonced = true;
foreach ($inlineScripts[0] as $scriptTag) {
if (strpos($scriptTag, 'nonce=') === false) {
$allNonced = false;
}
}
assertTrue($allNonced, "$file : tous les scripts inline portent un nonce CSP");
// Le gabarit doit être syntaxiquement valide
$lint = shell_exec(escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($path) . ' 2>&1');
assertContains('No syntax errors', $lint, "$file passe php -l");
}
// --- Blocage des artefacts de documentation (SEC-6) --------------------------
$nginxConf = file_get_contents($root . '/conf/nginx.conf.sample');
assertContains(
'^/[^/]+\.(adoc|html|pdf)$',
$nginxConf,
'nginx.conf.sample bloque les .adoc/.html/.pdf à la racine web'
);
$htaccess = file_get_contents($root . '/conf/.htaccess.sample');
assertContains(
'RewriteRule ^[^/]+\.(adoc|html|pdf)$ - [F,L,NC]',
$htaccess,
'.htaccess.sample bloque les .adoc/.html/.pdf à la racine web'
);
// --- dons.php : chemins d'erreur 404 et 500 (exécution réelle) ---------------
// dons.php inclut 404.php / 500.php (copies d'instance, non versionnées) :
// on les crée à partir des .sample si elles sont absentes, puis on nettoie.
$createdCopies = [];
foreach (['404.php', '500.php'] as $copy) {
$copyPath = $root . '/' . $copy;
if (!file_exists($copyPath)) {
copy($root . '/' . $copy . '.sample', $copyPath);
$createdCopies[] = $copyPath;
}
}
/**
* Exécute dons.php dans un sous-processus PHP avec des constantes prédéfinies
* (PEERTUBE_URL privée : aucun appel réseau, cf. bootstrap.php).
*
* @param array $defines Constantes définies avant le chargement de dons.php
* @return string Sortie complète du sous-processus (stdout + stderr)
*/
$runDonsPage = function (array $defines) use ($root): string {
$lines = ['<?php'];
foreach ($defines as $name => $value) {
$lines[] = 'define(' . var_export($name, true) . ', ' . var_export($value, true) . ');';
}
$lines[] = 'chdir(' . var_export($root, true) . ');';
$lines[] = 'require ' . var_export($root . '/dons.php', true) . ';';
$runner = tempnam(sys_get_temp_dir(), 'dons-runner-') . '.php';
file_put_contents($runner, implode("\n", $lines) . "\n");
$output = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($runner) . ' 2>&1');
unlink($runner);
return $output ?? '';
};
$noPlatform = [
'PEERTUBE_URL' => 'http://127.0.0.1', // URL privée volontaire : bloque tout appel API réel
'APP_HOST_NAME' => 'test.local',
'DONATIONS_ENABLED' => true,
'LIBERAPAY_URL' => '',
'KOFI_URL' => '',
'STRIPE_ENABLED' => false,
];
$phpErrorNeedles = ['PHP Warning', 'PHP Notice', 'PHP Deprecated', 'PHP Fatal error',
'Fatal error', 'Warning:', 'Notice:', 'Deprecated:', 'Uncaught'];
// Dons activés mais aucune plateforme configurée -> page 500 propre
$output500 = $runDonsPage($noPlatform);
assertContains(
'<p class="error-code" aria-hidden="true">500</p>',
$output500,
'dons.php sans plateforme configurée affiche la page 500'
);
foreach ($phpErrorNeedles as $needle) {
assertNotContains($needle, $output500, "page 500 sans erreur PHP ($needle)");
}
// Dons désactivés -> page 404 propre
$output404 = $runDonsPage(['DONATIONS_ENABLED' => false] + $noPlatform);
assertContains(
'<p class="error-code" aria-hidden="true">404</p>',
$output404,
'dons.php avec dons désactivés affiche la page 404'
);
foreach ($phpErrorNeedles as $needle) {
assertNotContains($needle, $output404, "page 404 sans erreur PHP ($needle)");
}
foreach ($createdCopies as $copyPath) {
unlink($copyPath);
}