fix: block doc artifacts and add error templates
This commit is contained in:
@@ -8,6 +8,8 @@ robots.txt
|
||||
site.webmanifest
|
||||
mentions-legales.php
|
||||
dons.php
|
||||
404.php
|
||||
500.php
|
||||
|
||||
# Fichiers de l'IDE/éditeur
|
||||
.vscode/
|
||||
@@ -37,6 +39,9 @@ uploads/*
|
||||
# vendor/
|
||||
# node_modules/
|
||||
|
||||
# Rapport d'audit interne OKI (ne pas committer)
|
||||
audit/
|
||||
|
||||
# Artefacts de tests locaux
|
||||
.pytest_cache/
|
||||
__pycache__/
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -22,6 +22,13 @@ RewriteRule ^(includes|cache|docs|conf)/ - [F,L]
|
||||
Require all denied
|
||||
</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
|
||||
Options -Indexes
|
||||
|
||||
|
||||
@@ -40,6 +40,16 @@ server {
|
||||
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
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
|
||||
+18
-16
@@ -123,7 +123,7 @@ function linkUrlsInText(string $text): string {
|
||||
?>
|
||||
|
||||
<!-- Script pour éviter le flash en mode sombre -->
|
||||
<script>
|
||||
<script nonce="<?php echo getCspNonce(); ?>">
|
||||
(function() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
@@ -223,10 +223,10 @@ function linkUrlsInText(string $text): string {
|
||||
|
||||
<!-- Onglets don ponctuel / mensuel -->
|
||||
<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
|
||||
</button>
|
||||
<button class="tab-btn" onclick="switchTab('monthly')">
|
||||
<button class="tab-btn" data-tab="monthly">
|
||||
<i class="fas fa-sync-alt"></i> Don mensuel
|
||||
</button>
|
||||
</div>
|
||||
@@ -308,20 +308,22 @@ function linkUrlsInText(string $text): string {
|
||||
<?php include 'includes/mobile-menu.php'; ?>
|
||||
|
||||
<!-- Script pour les onglets -->
|
||||
<script>
|
||||
function switchTab(tab) {
|
||||
// Cacher tous les contenus et désactiver tous les boutons
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
<script nonce="<?php echo getCspNonce(); ?>">
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
// Cacher tous les contenus et désactiver tous les boutons
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.tab-btn').forEach(otherBtn => {
|
||||
otherBtn.classList.remove('active');
|
||||
});
|
||||
|
||||
// Afficher le contenu sélectionné et activer le bouton
|
||||
document.getElementById(tab + '-tab').classList.add('active');
|
||||
event.target.classList.add('active');
|
||||
}
|
||||
// Afficher le contenu sélectionné et activer le bouton
|
||||
document.getElementById(this.dataset.tab + '-tab').classList.add('active');
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="js/main.js"></script>
|
||||
|
||||
@@ -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 & "échappé"',
|
||||
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(
|
||||
'<script>alert(1)</script>',
|
||||
linkUrlsInText('<script>alert(1)</script>'),
|
||||
'linkUrlsInText échappe le HTML injecté dans le texte'
|
||||
);
|
||||
|
||||
assertContains(
|
||||
'href="https://example.com/?a=1&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);
|
||||
}
|
||||
Reference in New Issue
Block a user