isHttpsRequest(); $headers = [ 'Content-Security-Policy' => $this->buildContentSecurityPolicy($isHttps), 'Referrer-Policy' => 'strict-origin-when-cross-origin', 'Permissions-Policy' => 'accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()', 'Cross-Origin-Opener-Policy' => 'same-origin', 'Cross-Origin-Resource-Policy' => 'same-origin', 'Origin-Agent-Cluster' => '?1', 'X-Content-Type-Options' => 'nosniff', 'X-Frame-Options' => 'SAMEORIGIN', 'X-Permitted-Cross-Domain-Policies' => 'none', ]; if ($isHttps) { $headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'; } return $headers; } public function send(array $overrides = [], ?bool $isHttps = null): void { if (headers_sent()) { return; } $headers = $this->buildHeaders($isHttps); foreach ($overrides as $name => $value) { if (!is_string($name) || trim($name) === '') { continue; } if ($value === null) { unset($headers[$name]); continue; } $headers[$name] = (string) $value; } header_remove('X-Powered-By'); foreach ($headers as $name => $value) { header($name . ': ' . $value, true); } } private function buildContentSecurityPolicy(bool $isHttps): string { $directives = [ "default-src 'self'", "base-uri 'self'", "form-action 'self'", "frame-ancestors 'self'", "object-src 'none'", "connect-src 'self'", "font-src 'self' data:", "img-src 'self' data:", "manifest-src 'self'", "script-src 'self'", "style-src 'self'", ]; if ($isHttps) { $directives[] = 'upgrade-insecure-requests'; } return implode('; ', $directives); } private function isHttpsRequest(): bool { $https = $_SERVER['HTTPS'] ?? null; if (is_string($https) && $https !== '' && strtolower($https) !== 'off') { return true; } $forwardedProto = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''; if (is_string($forwardedProto) && strtolower($forwardedProto) === 'https') { return true; } return (string) ($_SERVER['SERVER_PORT'] ?? '') === '443'; } }