Files
cacti/tests/Unit/CspReportEndpointTest.php
T
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

95 lines
3.1 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/* Prevent the entry-point block from executing when the file is included
* during testing. The guard in csp_report_endpoint.php checks for this
* constant and returns before touching $_SERVER or php://input. */
define('CACTI_CSP_REPORT_TEST_MODE', 1);
if (!function_exists('cacti_log')) {
function cacti_log($message, $stdout = false, $facility = 'CACTI') {
/* intentional no-op in test context */
}
}
require_once __DIR__ . '/../../lib/csp_report_endpoint.php';
function _csp_headers($ct) {
return array('CONTENT_TYPE' => $ct);
}
test('validator rejects oversize body', function () {
$body = str_repeat('a', 20000);
$result = csp_report_validate_payload(_csp_headers('application/csp-report'), $body, 16384);
expect($result['ok'])->toBeFalse();
expect(strtolower($result['reason']))->toContain('size');
});
test('validator rejects wrong content type', function () {
$result = csp_report_validate_payload(_csp_headers('text/plain'), '{"csp-report":{}}', 16384);
expect($result['ok'])->toBeFalse();
expect(strtolower($result['reason']))->toContain('content-type');
});
test('validator rejects malformed JSON', function () {
$result = csp_report_validate_payload(_csp_headers('application/csp-report'), 'not json{', 16384);
expect($result['ok'])->toBeFalse();
expect(strtolower($result['reason']))->toContain('json');
});
test('validator accepts legacy csp-report format', function () {
$body = json_encode(array(
'csp-report' => array(
'violated-directive' => 'script-src',
'blocked-uri' => 'inline',
'document-uri' => 'https://x/y',
),
));
$result = csp_report_validate_payload(_csp_headers('application/csp-report'), $body, 16384);
expect($result['ok'])->toBeTrue();
expect($result['summary'])->toContain('script-src');
expect($result['summary'])->toContain('inline');
expect($result['summary'])->toContain('https://x/y');
});
test('validator accepts Reporting API body format', function () {
$body = json_encode(array(
'type' => 'csp-violation',
'body' => array(
'effectiveDirective' => 'script-src',
'blockedURL' => 'https://evil.example/x.js',
'documentURL' => 'https://app.example/page',
),
));
$result = csp_report_validate_payload(_csp_headers('application/json'), $body, 16384);
expect($result['ok'])->toBeTrue();
expect($result['summary'])->toContain('script-src');
});
test('sanitizer strips CR/LF so a report cannot forge log lines', function () {
$dirty = "script-src\r\nFORGED LOG LINE";
$clean = csp_report_sanitize_field($dirty);
expect($clean)->not->toContain("\r");
expect($clean)->not->toContain("\n");
});
test('sanitizer truncates overlong fields', function () {
$clean = csp_report_sanitize_field(str_repeat('a', 500));
expect(strlen($clean))->toBeLessThanOrEqual(256);
});