98 lines
2.8 KiB
PHP
98 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
final class SecurityHeadersService
|
|
{
|
|
public function buildHeaders(?bool $isHttps = null): array
|
|
{
|
|
$isHttps = $isHttps ?? $this->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';
|
|
}
|
|
}
|