blocks in layout.php must carry the SAME nonce as the // header, or the page breaks. Fresh per request: a reused nonce is no better than // 'unsafe-inline' once an attacker can read one. $a = Csp::nonce(); $this->assertSame($a, Csp::nonce()); Csp::reset(); $this->assertNotSame($a, Csp::nonce()); } public function testPolicyForbidsInlineAndObjectsAndFraming(): void { $p = Csp::policy('abc123'); $this->assertStringContainsString("script-src 'self' 'nonce-abc123'", $p); $this->assertStringNotContainsString('unsafe-inline', $p); $this->assertStringNotContainsString('unsafe-eval', $p); $this->assertStringContainsString("object-src 'none'", $p); $this->assertStringContainsString("base-uri 'none'", $p); $this->assertStringContainsString("frame-ancestors 'none'", $p); // Air-gapped delivery: nothing may be fetched from a third-party origin. $this->assertStringNotContainsString('http://', $p); $this->assertStringNotContainsString('https://', $p); } public function testHtmlResponsesCarryTheSecurityHeaders(): void { $h = Response::text('

hi

', 200, Csp::headers())->headers(); $this->assertSame('text/html; charset=utf-8', $h['Content-Type']); $this->assertArrayHasKey('Content-Security-Policy', $h); $this->assertSame('nosniff', $h['X-Content-Type-Options']); $this->assertSame('DENY', $h['X-Frame-Options']); $this->assertStringContainsString("'nonce-" . Csp::nonce() . "'", $h['Content-Security-Policy']); } public function testEveryInlineScriptInTheViewsCarriesANonce(): void { // The policy is only as good as the markup: one un-nonced inline block and the page // silently loses functionality in production while looking fine in a dev server that // sends no CSP. No build step means no bundler would catch this either. foreach (glob(dirname(__DIR__) . '/src/View/*.php') as $view) { $src = (string)file_get_contents($view); preg_match_all('/]*\bsrc=)([^>]*)>/i', $src, $m); foreach ($m[1] as $attrs) { $this->assertStringContainsString('nonce=', $attrs, basename($view) . ' has an inline