Files
Thomas Vincent d4e255b5d9 security: stage nonce CSP migration and gated enforcement (#7662)
* security: add nonce CSP mode for inline JavaScript protection

Reworks the content_security_policy_script setting into a three-mode Inline JavaScript Protection dropdown (HTMLX default, Nonce, None) and ports the per-request nonce, strict-dynamic policy, and CSP violation report endpoint from 1.2.x. The stored option key and its default are unchanged, so existing installs keep their current policy byte-for-byte; nonce mode is opt-in.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* security: stage nonce CSP enforcement safely

Add report-only migration, gated enforcement, nonce-aware shared emitters, Linux Docker validation, and operator documentation.\n\nRefs #7703

* security: validate CSP header sources

Reject malformed alternate-source configurations at the CSP boundary, validate fallback report URIs, and remove the obsolete Psalm header-taint baseline entry.\n\nRefs #7705

* chore: drop the composer.json change from this PR

Dependency and manifest changes are a maintainer call and do not belong in
this one.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-08-16 21:38:46 -04:00

76 lines
3.4 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Stand-alone fixture served by the built-in PHP web server
* (`php -S 127.0.0.1:<port> -t tests/integration/fixtures csp_fixture.php`).
*
* The active CSP mode comes from the CSP_TEST_MODE environment variable so
* the harness can drive every branch of the setting without a database. The
* header and <meta> lines below mirror include/global.php and lib/html.php,
* so the test proves the real wire format for each mode over HTTP.
*
* Dependency-free on purpose: only lib/headers_secure.php is loaded so a
* failure points at the header helper, not the Cacti bootstrap.
*/
require_once __DIR__ . '/../../../lib/headers_secure.php';
if (!defined('CACTI_CSP_NONCE_ENFORCE')) {
define('CACTI_CSP_NONCE_ENFORCE', getenv('CSP_TEST_ENFORCE') === '1');
}
if (!function_exists('read_config_option')) {
function read_config_option($key) {
switch ($key) {
case 'content_security_policy_script':
$v = getenv('CSP_TEST_MODE');
return $v === false ? '' : $v;
case 'content_security_alternate_sources':
$v = getenv('CSP_TEST_ALTERNATES');
return $v === false ? '' : $v;
case 'content_security_report_uri':
$v = getenv('CSP_TEST_REPORT_URI');
return $v === false ? '' : $v;
}
return '';
}
}
$alternates = CactiSecureHeaders::normalizeAlternateSources(read_config_option('content_security_alternate_sources'));
$script_src = CactiSecureHeaders::scriptSrc($alternates);
$report_uri = CactiSecureHeaders::getCspMode() === 'nonce-enforce' ?
CactiSecureHeaders::reportUriDirective() : '';
// Mirrors the HTTP header emitted by include/global.php.
header("Content-Security-Policy: default-src *; img-src 'self' https://api.qrserver.com $alternates data: blob:; style-src 'self' 'unsafe-inline' $alternates; $script_src; frame-ancestors 'self' $alternates; worker-src 'self' $alternates;$report_uri");
$report_script_src = CactiSecureHeaders::reportOnlyScriptSrc($alternates);
if ($report_script_src !== '') {
$report_uri = CactiSecureHeaders::reportUriDirective();
header("Content-Security-Policy-Report-Only: default-src *; img-src 'self' https://api.qrserver.com $alternates data: blob:; style-src 'self' 'unsafe-inline' $alternates; $report_script_src; frame-ancestors 'self' $alternates; worker-src 'self' $alternates;$report_uri");
}
$nonce_attr = CactiSecureHeaders::getNonceAttribute();
print "<!doctype html>\n<html><head>\n";
/* Mirrors the <meta> policy in lib/html.php: no frame-ancestors/report-uri,
* both of which are invalid inside a meta CSP. */
print "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src *; img-src 'self' https://api.qrserver.com $alternates data: blob:; style-src 'self' 'unsafe-inline' $alternates; $script_src; worker-src 'self' $alternates;\">\n";
print "</head><body>\n";
print "<script type='text/javascript' $nonce_attr>var x = 1;</script>\n";
print "<script type='text/javascript'>window.legacyInlineRan = true;</script>\n";
print "<button type='button' onclick='window.legacyHandlerRan = true;'>Legacy handler</button>\n";
print "</body></html>\n";