Files
wms-app/app/assets/utils/page_headers.php
T
Thanakorn f11af6e949 Self-host front-end libraries, minimal sign-in header and CSP
- 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
2026-09-24 14:53:41 +07:00

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);
}
}