- 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.
66 lines
2.3 KiB
PHP
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');
|
|
}
|
|
}
|