Files
theoldfitz/inc/security-headers.php
T
David Dyer c7b836bcf2 Refactor admin pages to use shared head and navigation components
- Created `admin-head.php` for consistent <head> markup across admin pages.
- Created `admin-nav.php` for shared navigation bar, reducing duplication.
- Updated all admin pages (account, hours, index, login, menu, users) to utilize the new components.
- Added security headers to `auth.php` and `index.php` for improved security.
- Introduced `security-headers.php` to manage security headers across the application.
- Moved inline styles from admin pages to SCSS files to comply with Content Security Policy (CSP).
- Normalized file permissions during build process to prevent permission issues on deployment.
2026-10-25 16:06:15 -04:00

66 lines
2.3 KiB
PHP

<?php
declare(strict_types=1);
/**
* Security headers for every page, sent from PHP so they apply the same
* under NPM, Caddy, or local dev. Public pages call send_security_headers()
* directly; admin pages get it through inc/auth.php.
*
* Static files (CSS, JS, images) are served by Apache and don't pass
* through here — that's fine, since the CSP only matters on documents.
*/
/**
* Whether the visitor reached us over HTTPS. TLS ends at the reverse proxy
* (NPM or Caddy), so PHP itself only ever sees plain HTTP — trust the
* proxy's X-Forwarded-Proto, but only from a private/loopback address so a
* public client can't claim it.
*/
function request_is_https(): bool {
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
return true;
}
$remote = $_SERVER['REMOTE_ADDR'] ?? '';
$fromProxy = filter_var($remote, FILTER_VALIDATE_IP)
&& !filter_var($remote, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
return $fromProxy && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https';
}
function send_security_headers(): void {
if (headers_sent()) {
return;
}
// Under `npm run dev`, browser-sync injects an inline <script> to load
// its live-reload client and talks over a websocket. Allow exactly that
// in dev only; production stays strict. Inline *styles* are blocked in
// dev too, so a stray style="" still shows up while developing.
$isDev = PHP_SAPI === 'cli-server';
$csp = [
"default-src 'self'",
"script-src 'self'" . ($isDev ? " 'unsafe-inline'" : ''),
"style-src 'self' https://fonts.googleapis.com",
"font-src https://fonts.gstatic.com",
"img-src 'self'",
"connect-src 'self'" . ($isDev ? ' ws: wss:' : ''),
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
];
header('Content-Security-Policy: ' . implode('; ', $csp));
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
// HSTS only over HTTPS (browsers ignore it on HTTP anyway). No
// includeSubDomains: this site is one subdomain of daviddyer.me and
// shouldn't dictate policy for the others.
if (request_is_https()) {
header('Strict-Transport-Security: max-age=31536000');
}
}