Paillasse/portail/app/Http/Middleware/PolitiqueSecuriteContenu.php
Adam Belkacemi ec4eae3744 ajoute la visio de classe : Jitsi auto-hébergé, jetons du portail, prof seul modérateur
Jitsi (docker-jitsi-meet, version figée) derrière Nginx sur meet.syllabuslab.fr,
accès par jeton JWT signé par le portail, modération du micro, mains levées
synchronisées avec l'application. Pare-feu : UDP 10000 et pont interne
br-visio (accords d'Adam). Jicofo n'authentifie plus lui-même : il donnait
le rôle de propriétaire aux élèves. Capacité ramenée à 20 VM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:35:35 +02:00

96 lines
3.5 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',
];
// Visio (étape 15) : script d'intégration et cadre servis par Jitsi.
$visio = config('visio.domaine') ? ' https://'.config('visio.domaine') : '';
$directives = $this->estApplication($request)
? [
'script-src' => "'self' 'nonce-".Vite::cspNonce()."'".$visio,
'frame-src' => "'self'".$visio,
// 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) || str_starts_with((string) $route->getName(), 'rejoindre')) {
return true;
}
return collect($route->gatherMiddleware())
->contains(fn ($middleware) => is_string($middleware) && str_starts_with($middleware, 'auth:'));
}
}