headers->get('Content-Security-Policy'))) ->mapWithKeys(function (string $d) { $morceaux = preg_split('/\s+/', trim($d), 2); return [$morceaux[0] => $morceaux[1] ?? '']; })->all(); } test("les pages de l'application ont une CSP stricte : pas de script en ligne sans le nonce", function () { $eleve = Eleve::factory()->create(); $jeton = $eleve->demarreNouvelleSession(); $reponse = $this->withSession(['jeton_session_eleve' => $jeton])->actingAs($eleve, 'eleve')->get(route('eleve')); $csp = politique($reponse); expect($csp['script-src'])->toMatch("/^'self' 'nonce-[A-Za-z0-9+\\/=]+'$/") ->and($csp['script-src'])->not->toContain('unsafe-inline') ->and($csp['object-src'])->toBe("'none'") ->and($csp['frame-ancestors'])->toBe("'none'"); }); test('la page de connexion est stricte aussi, la vitrine tolère les scripts en ligne', function () { expect(politique($this->get(route('login')))['script-src'])->not->toContain('unsafe-inline') ->and(politique($this->get(route('accueil')))['script-src'])->toContain("'unsafe-inline'"); }); test("chaque script de la page d'inscription porte le nonce de l'en-tête", function () { $reponse = $this->get(route('register')); preg_match("/'nonce-([^']+)'/", $reponse->headers->get('Content-Security-Policy'), $m); preg_match_all('/]*>/', $reponse->getContent(), $scripts); expect($scripts[0])->not->toBeEmpty(); foreach ($scripts[0] as $balise) { expect($balise)->toContain("nonce=\"{$m[1]}\""); } }); test('le rapport de violation est accepté sans jeton CSRF', function () { $this->call('POST', route('csp.rapport'), [], [], [], ['CONTENT_TYPE' => 'application/csp-report'], json_encode(['csp-report' => ['document-uri' => 'https://syllabuslab.fr/eleve?x=1', 'violated-directive' => 'script-src', 'blocked-uri' => 'inline']])) ->assertNoContent(); });