Base de travail V7.8 reprise du répertoire de travail
Commit technique : reproduit à l'identique l'état non commité de la copie de travail (sécurisation V7.8, CSP à nonce, Session, migration des droits) afin que le travail d'ergonomie qui suit soit isolable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
89
app/Core/Security.php
Normal file
89
app/Core/Security.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* En-têtes de sécurité des réponses HTML.
|
||||
*
|
||||
* La CSP est nominative : aucun script inline n'est autorisé sans le nonce
|
||||
* généré à chaque requête. PTA ne contient aucun gestionnaire d'événement
|
||||
* inline (onclick=, onchange=…) ni style inline, la politique peut donc rester
|
||||
* stricte sans dégrader l'interface.
|
||||
*/
|
||||
final class Security
|
||||
{
|
||||
private static ?string $nonce = null;
|
||||
|
||||
public static function nonce(): string
|
||||
{
|
||||
if (self::$nonce === null) {
|
||||
self::$nonce = base64_encode(random_bytes(16));
|
||||
}
|
||||
|
||||
return self::$nonce;
|
||||
}
|
||||
|
||||
public static function sendHtmlHeaders(): void
|
||||
{
|
||||
if (headers_sent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nonce = self::nonce();
|
||||
$policy = [
|
||||
"default-src 'self'",
|
||||
"base-uri 'self'",
|
||||
"object-src 'none'",
|
||||
"frame-ancestors 'none'",
|
||||
"form-action 'self'",
|
||||
"img-src 'self' data:",
|
||||
"style-src 'self'",
|
||||
"font-src 'self'",
|
||||
// Aucune requête sortante depuis le navigateur : les appels à
|
||||
// data.education.gouv.fr sont faits côté serveur.
|
||||
"connect-src 'self'",
|
||||
sprintf("script-src 'self' 'nonce-%s'", $nonce),
|
||||
];
|
||||
|
||||
header('Content-Security-Policy: ' . implode('; ', $policy));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('Referrer-Policy: no-referrer');
|
||||
header('Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=()');
|
||||
header('Cross-Origin-Opener-Policy: same-origin');
|
||||
|
||||
if (Session::isHttps()) {
|
||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrai uniquement si le mode debug a été activé explicitement.
|
||||
* Par défaut, aucun détail technique ne sort du serveur.
|
||||
*/
|
||||
public static function debugEnabled(): bool
|
||||
{
|
||||
return in_array(strtolower((string) (getenv('PTA_DEBUG') ?: '0')), ['1', 'true', 'on', 'yes'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Journalise l'exception côté serveur et renvoie le message à afficher.
|
||||
* Le détail technique n'est rendu que si PTA_DEBUG est explicitement actif.
|
||||
*/
|
||||
public static function reportThrowable(\Throwable $exception, string $publicMessage): string
|
||||
{
|
||||
error_log(sprintf(
|
||||
'[PTA] %s: %s in %s:%d',
|
||||
get_class($exception),
|
||||
$exception->getMessage(),
|
||||
$exception->getFile(),
|
||||
$exception->getLine()
|
||||
));
|
||||
|
||||
return self::debugEnabled()
|
||||
? $publicMessage . ' Détail : ' . $exception->getMessage()
|
||||
: $publicMessage;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user