88 lines
3 KiB
PHP
88 lines
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:",
|
||
|
|
'font-src' => "'self' https://fonts.gstatic.com",
|
||
|
|
'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' https://fonts.googleapis.com",
|
||
|
|
'connect-src' => "'self' wss://{$request->getHost()}",
|
||
|
|
]
|
||
|
|
: [
|
||
|
|
'script-src' => "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdn.tailwindcss.com",
|
||
|
|
'style-src' => "'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net",
|
||
|
|
'connect-src' => "'self'",
|
||
|
|
'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:'));
|
||
|
|
}
|
||
|
|
}
|