- sign-in pages load only what they use (include_login_header.php, ajax_core.js) - every library, font and data file served from assets/vendor with pinned versions (VERSIONS.json); no CDN at runtime - Content-Security-Policy on app, sign-in and landing pages - remove httpbin Dropzone target and source-map references
55 lines
2.2 KiB
PHP
55 lines
2.2 KiB
PHP
<?php
|
|
/**
|
|
* page_headers.php — security headers for HTML pages (app pages, sign-in pages).
|
|
*
|
|
* Call send_page_security_headers() before any output. The Content-Security-Policy
|
|
* lists what the pages actually load:
|
|
* - scripts, styles, fonts and data files are all self-hosted under assets/vendor/
|
|
* (versions in assets/vendor/VERSIONS.json), so no CDN host is allowed;
|
|
* - the Node.js real-time server (NODE_PUBLIC_URL) serves socket.io.js and the
|
|
* WebSocket connection;
|
|
* - 'unsafe-inline' because pages use inline <script> blocks and onclick=
|
|
* handlers; 'unsafe-eval' because alasql compiles its queries with new Function.
|
|
*/
|
|
|
|
if (!function_exists('send_page_security_headers')) {
|
|
function send_page_security_headers(): void {
|
|
if (headers_sent()) return;
|
|
|
|
$script = ["'self'", "'unsafe-inline'", "'unsafe-eval'"];
|
|
$connect = ["'self'"];
|
|
|
|
if (defined('NODE_PUBLIC_URL')) {
|
|
$node = parse_url(NODE_PUBLIC_URL);
|
|
if (!empty($node['scheme']) && !empty($node['host'])) {
|
|
$origin = $node['host'] . (isset($node['port']) ? ':' . $node['port'] : '');
|
|
$secure = strtolower($node['scheme']) === 'https';
|
|
$script[] = ($secure ? 'https://' : 'http://') . $origin;
|
|
$connect[] = ($secure ? 'https://' : 'http://') . $origin;
|
|
$connect[] = ($secure ? 'wss://' : 'ws://') . $origin;
|
|
}
|
|
}
|
|
|
|
$csp = implode('; ', [
|
|
"default-src 'self'",
|
|
'script-src ' . implode(' ', $script),
|
|
"style-src 'self' 'unsafe-inline'",
|
|
"font-src 'self' data:",
|
|
"img-src 'self' data: blob:",
|
|
"media-src 'self' blob:",
|
|
'connect-src ' . implode(' ', $connect),
|
|
"worker-src 'self' blob:",
|
|
"frame-src 'self' blob:",
|
|
"object-src 'none'",
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
"frame-ancestors 'self'",
|
|
]);
|
|
|
|
header('Content-Security-Policy: ' . $csp, true);
|
|
header('X-Content-Type-Options: nosniff', true);
|
|
header('X-Frame-Options: SAMEORIGIN', true);
|
|
header('Referrer-Policy: strict-origin-when-cross-origin', true);
|
|
}
|
|
}
|