makeRequest('/'); $cspHeader = $this->getHeader($response, 'Content-Security-Policy'); if (empty($cspHeader)) { throw new \RuntimeException('Content-Security-Policy header missing'); } // Check for essential CSP directives $requiredDirectives = [ 'default-src', 'script-src', 'style-src', 'img-src', 'connect-src', 'font-src', ]; $missingDirectives = []; foreach ($requiredDirectives as $directive) { if (!str_contains($cspHeader, $directive)) { $missingDirectives[] = $directive; } } if (!empty($missingDirectives)) { throw new \RuntimeException( 'CSP header missing required directives: ' . implode(', ', $missingDirectives) ); } // Check for unsafe-inline in script-src (should be avoided) if (str_contains($cspHeader, "script-src 'unsafe-inline'") && !str_contains($cspHeader, 'nonce-')) { echo "⚠️ Warning: CSP uses 'unsafe-inline' for scripts without nonce\n"; } else { echo "✅ Content-Security-Policy header properly configured\n"; } } /** * Test Strict-Transport-Security (HSTS) header */ public function testStrictTransportSecurityHeader(): void { $response = $this->makeRequest('/'); $hstsHeader = $this->getHeader($response, 'Strict-Transport-Security'); if (empty($hstsHeader)) { throw new \RuntimeException('Strict-Transport-Security header missing'); } // Check for required directives if (!str_contains($hstsHeader, 'max-age=')) { throw new \RuntimeException('HSTS header missing max-age directive'); } // Extract max-age value preg_match('/max-age=(\d+)/', $hstsHeader, $matches); $maxAge = (int) ($matches[1] ?? 0); if ($maxAge < 31536000) { // 1 year echo "⚠️ Warning: HSTS max-age is less than 1 year ({$maxAge}s)\n"; } // Check for includeSubDomains if (!str_contains($hstsHeader, 'includeSubDomains')) { echo "⚠️ Warning: HSTS header missing 'includeSubDomains' directive\n"; } // Check for preload if (!str_contains($hstsHeader, 'preload')) { echo "ℹ️ Info: HSTS header missing 'preload' directive (optional)\n"; } echo "✅ Strict-Transport-Security header present\n"; } /** * Test X-Frame-Options header */ public function testXFrameOptionsHeader(): void { $response = $this->makeRequest('/'); $xFrameOptions = $this->getHeader($response, 'X-Frame-Options'); if (empty($xFrameOptions)) { throw new \RuntimeException('X-Frame-Options header missing'); } $validValues = ['DENY', 'SAMEORIGIN']; if (!in_array(strtoupper($xFrameOptions), $validValues)) { throw new \RuntimeException( "X-Frame-Options has invalid value: {$xFrameOptions} " . "(expected: DENY or SAMEORIGIN)" ); } echo "✅ X-Frame-Options header set to {$xFrameOptions}\n"; } /** * Test X-Content-Type-Options header */ public function testXContentTypeOptionsHeader(): void { $response = $this->makeRequest('/'); $xContentTypeOptions = $this->getHeader($response, 'X-Content-Type-Options'); if (empty($xContentTypeOptions)) { throw new \RuntimeException('X-Content-Type-Options header missing'); } if (strtolower($xContentTypeOptions) !== 'nosniff') { throw new \RuntimeException( "X-Content-Type-Options has invalid value: {$xContentTypeOptions} " . "(expected: nosniff)" ); } echo "✅ X-Content-Type-Options header set to nosniff\n"; } /** * Test X-XSS-Protection header (legacy, but still useful) */ public function testXXssProtectionHeader(): void { $response = $this->makeRequest('/'); $xXssProtection = $this->getHeader($response, 'X-XSS-Protection'); if (!empty($xXssProtection)) { if (!str_contains($xXssProtection, '1')) { echo "⚠️ Warning: X-XSS-Protection not enabled\n"; } else { echo "✅ X-XSS-Protection header present\n"; } } else { echo "ℹ️ Info: X-XSS-Protection header missing (legacy, CSP is preferred)\n"; } } /** * Test Referrer-Policy header */ public function testReferrerPolicyHeader(): void { $response = $this->makeRequest('/'); $referrerPolicy = $this->getHeader($response, 'Referrer-Policy'); if (empty($referrerPolicy)) { throw new \RuntimeException('Referrer-Policy header missing'); } $recommendedPolicies = [ 'no-referrer', 'no-referrer-when-downgrade', 'strict-origin', 'strict-origin-when-cross-origin', 'same-origin' ]; if (!in_array(strtolower($referrerPolicy), $recommendedPolicies)) { echo "⚠️ Warning: Referrer-Policy value '{$referrerPolicy}' may not be optimal\n"; } else { echo "✅ Referrer-Policy header set to {$referrerPolicy}\n"; } } /** * Test Permissions-Policy header (formerly Feature-Policy) */ public function testPermissionsPolicyHeader(): void { $response = $this->makeRequest('/'); $permissionsPolicy = $this->getHeader($response, 'Permissions-Policy'); if (empty($permissionsPolicy)) { echo "ℹ️ Info: Permissions-Policy header missing (recommended for production)\n"; return; } // Check for common privacy-sensitive features $sensitiveFeatures = ['geolocation', 'microphone', 'camera', 'payment']; foreach ($sensitiveFeatures as $feature) { if (str_contains($permissionsPolicy, "{$feature}=()")) { echo "✅ Permissions-Policy restricts '{$feature}' access\n"; } } } /** * Test that Server header is masked or removed */ public function testServerHeaderMasked(): void { $response = $this->makeRequest('/'); $serverHeader = $this->getHeader($response, 'Server'); if (!empty($serverHeader)) { // Check if it reveals sensitive information $sensitiveTerms = ['nginx/', 'apache/', 'php/', 'version', 'ubuntu', 'debian']; foreach ($sensitiveTerms as $term) { if (stripos($serverHeader, $term) !== false) { echo "⚠️ Warning: Server header reveals version info: {$serverHeader}\n"; return; } } echo "✅ Server header present but masked: {$serverHeader}\n"; } else { echo "✅ Server header removed (best practice)\n"; } } /** * Test that X-Powered-By header is removed */ public function testXPoweredByHeaderRemoved(): void { $response = $this->makeRequest('/'); $xPoweredBy = $this->getHeader($response, 'X-Powered-By'); if (!empty($xPoweredBy)) { throw new \RuntimeException( "X-Powered-By header should be removed, found: {$xPoweredBy}" ); } echo "✅ X-Powered-By header removed\n"; } /** * Test Cross-Origin-Resource-Policy header */ public function testCrossOriginResourcePolicyHeader(): void { $response = $this->makeRequest('/'); $corp = $this->getHeader($response, 'Cross-Origin-Resource-Policy'); if (!empty($corp)) { $validValues = ['same-origin', 'same-site', 'cross-origin']; if (!in_array(strtolower($corp), $validValues)) { echo "⚠️ Warning: Invalid Cross-Origin-Resource-Policy value: {$corp}\n"; } else { echo "✅ Cross-Origin-Resource-Policy set to {$corp}\n"; } } else { echo "ℹ️ Info: Cross-Origin-Resource-Policy header missing (recommended)\n"; } } /** * Test Cross-Origin-Embedder-Policy header */ public function testCrossOriginEmbedderPolicyHeader(): void { $response = $this->makeRequest('/'); $coep = $this->getHeader($response, 'Cross-Origin-Embedder-Policy'); if (!empty($coep)) { echo "✅ Cross-Origin-Embedder-Policy header present: {$coep}\n"; } else { echo "ℹ️ Info: Cross-Origin-Embedder-Policy header missing (advanced security)\n"; } } /** * Test Cross-Origin-Opener-Policy header */ public function testCrossOriginOpenerPolicyHeader(): void { $response = $this->makeRequest('/'); $coop = $this->getHeader($response, 'Cross-Origin-Opener-Policy'); if (!empty($coop)) { echo "✅ Cross-Origin-Opener-Policy header present: {$coop}\n"; } else { echo "ℹ️ Info: Cross-Origin-Opener-Policy header missing (advanced security)\n"; } } /** * Run all security headers tests */ public function runAllTests(): array { $results = []; try { $this->testContentSecurityPolicyHeader(); $results['csp'] = 'PASS'; } catch (\Exception $e) { $results['csp'] = 'FAIL: ' . $e->getMessage(); } try { $this->testStrictTransportSecurityHeader(); $results['hsts'] = 'PASS'; } catch (\Exception $e) { $results['hsts'] = 'FAIL: ' . $e->getMessage(); } try { $this->testXFrameOptionsHeader(); $results['x_frame_options'] = 'PASS'; } catch (\Exception $e) { $results['x_frame_options'] = 'FAIL: ' . $e->getMessage(); } try { $this->testXContentTypeOptionsHeader(); $results['x_content_type_options'] = 'PASS'; } catch (\Exception $e) { $results['x_content_type_options'] = 'FAIL: ' . $e->getMessage(); } try { $this->testXXssProtectionHeader(); $results['x_xss_protection'] = 'PASS'; } catch (\Exception $e) { $results['x_xss_protection'] = 'FAIL: ' . $e->getMessage(); } try { $this->testReferrerPolicyHeader(); $results['referrer_policy'] = 'PASS'; } catch (\Exception $e) { $results['referrer_policy'] = 'FAIL: ' . $e->getMessage(); } try { $this->testPermissionsPolicyHeader(); $results['permissions_policy'] = 'PASS'; } catch (\Exception $e) { $results['permissions_policy'] = 'FAIL: ' . $e->getMessage(); } try { $this->testServerHeaderMasked(); $results['server_header'] = 'PASS'; } catch (\Exception $e) { $results['server_header'] = 'FAIL: ' . $e->getMessage(); } try { $this->testXPoweredByHeaderRemoved(); $results['x_powered_by'] = 'PASS'; } catch (\Exception $e) { $results['x_powered_by'] = 'FAIL: ' . $e->getMessage(); } try { $this->testCrossOriginResourcePolicyHeader(); $results['corp'] = 'PASS'; } catch (\Exception $e) { $results['corp'] = 'FAIL: ' . $e->getMessage(); } try { $this->testCrossOriginEmbedderPolicyHeader(); $results['coep'] = 'PASS'; } catch (\Exception $e) { $results['coep'] = 'FAIL: ' . $e->getMessage(); } try { $this->testCrossOriginOpenerPolicyHeader(); $results['coop'] = 'PASS'; } catch (\Exception $e) { $results['coop'] = 'FAIL: ' . $e->getMessage(); } return $results; } private function makeRequest(string $uri): array { // Simulate HTTP response with headers return [ 'status' => 200, 'headers' => [ 'Content-Security-Policy' => "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self' https://fonts.gstatic.com;", 'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload', 'X-Frame-Options' => 'SAMEORIGIN', 'X-Content-Type-Options' => 'nosniff', 'X-XSS-Protection' => '1; mode=block', 'Referrer-Policy' => 'strict-origin-when-cross-origin', 'Permissions-Policy' => 'geolocation=(), microphone=(), camera=(), payment=()', 'Server' => 'CustomFramework', // X-Powered-By should NOT be present 'Cross-Origin-Resource-Policy' => 'same-origin', 'Cross-Origin-Embedder-Policy' => 'require-corp', 'Cross-Origin-Opener-Policy' => 'same-origin', ], 'body' => '' ]; } private function getHeader(array $response, string $headerName): string { return $response['headers'][$headerName] ?? ''; } }