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>
92 lines
3.3 KiB
PHP
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:'));
|
|
}
|
|
}
|