Paillasse/portail/app/Http/Middleware/PolitiqueSecuriteContenu.php
Adam Belkacemi e4a6aaebb1 ajoute les pages légales, la purge RGPD et sert polices et scripts depuis le portail
Pages /legal et /confidentialite réécrites d'après le système réel, purge
des classes archivées depuis 90 jours (prouvée par Pest), archivage côté
prof. Polices, xterm.js et CodeMirror ne sont plus chargés depuis Google
ni jsDelivr. Pages d'aperçu signalées, sans vrais établissements ni faux
chiffres. Journaux Laravel gardés 14 jours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:07:25 +02:00

92 lines
3.3 KiB
PHP

<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
/**
* Content-Security-Policy, en deux niveaux (étape 12) :
*
* - application (connexion, inscription, toute page authentifiée) : seuls
* nos scripts et ceux qui portent le nonce de la requête s'exécutent. Une
* injection de script dans un nom d'élève, par exemple, serait bloquée
* par le navigateur ;
* - vitrine (pages reprises du design) : scripts en ligne et CDN autorisés,
* faute de pouvoir les réécrire ici. Ces pages n'affichent aucune donnée
* d'utilisateur.
*
* Les violations sont envoyées à /csp-rapport (journal Laravel).
* Les autres en-têtes (HSTS, nosniff…) sont posés par Nginx.
*/
class PolitiqueSecuriteContenu
{
public function handle(Request $request, Closure $next): Response
{
Vite::useCspNonce();
$reponse = $next($request);
if (! $reponse->headers->has('Content-Security-Policy')) {
$reponse->headers->set('Content-Security-Policy', $this->politique($request));
}
return $reponse;
}
private function politique(Request $request): string
{
$communes = [
'default-src' => "'self'",
'img-src' => "'self' data:",
// Polices servies par le portail (Vite), depuis l'étape 14.
'font-src' => "'self'",
'object-src' => "'none'",
'base-uri' => "'self'",
'form-action' => "'self'",
'frame-ancestors' => "'none'",
'report-uri' => '/csp-rapport',
];
$directives = $this->estApplication($request)
? [
'script-src' => "'self' 'nonce-".Vite::cspNonce()."'",
// Attributs style= présents dans le gabarit commun : sans
// risque d'exécution de code, tolérés.
'style-src' => "'self' 'unsafe-inline'",
'connect-src' => "'self' wss://{$request->getHost()}",
]
: [
// jsDelivr : Pyodide et sql.js, téléchargés par /ide à la
// demande (WebAssembly, d'où 'wasm-unsafe-eval', qui n'autorise
// pas eval() en JavaScript). Tailwind : CDN d'exécution des
// pages du design. Voir docs/dette.md.
'script-src' => "'self' 'unsafe-inline' 'wasm-unsafe-eval' https://cdn.jsdelivr.net https://cdn.tailwindcss.com",
'style-src' => "'self' 'unsafe-inline'",
'connect-src' => "'self' https://cdn.jsdelivr.net",
'frame-src' => 'https://www.youtube-nocookie.com',
];
return collect($communes + $directives)
->map(fn (string $valeur, string $directive) => "{$directive} {$valeur}")
->implode('; ');
}
private function estApplication(Request $request): bool
{
$route = $request->route();
if ($route === null) {
return false;
}
if (in_array($route->getName(), ['login', 'register'], true)) {
return true;
}
return collect($route->gatherMiddleware())
->contains(fn ($middleware) => is_string($middleware) && str_starts_with($middleware, 'auth:'));
}
}