diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php index 04d93365af25..203e867372da 100644 --- a/system/HTTP/ContentSecurityPolicy.php +++ b/system/HTTP/ContentSecurityPolicy.php @@ -1052,4 +1052,9 @@ public function clearDirective(string $directive): void $this->{$this->directives[$directive]} = []; } + + public function clearNoncePlaceholders(string $text): string + { + return str_replace([$this->styleNonceTag, $this->scriptNonceTag], '', $text); + } } diff --git a/system/HTTP/ResponseTrait.php b/system/HTTP/ResponseTrait.php index 211663bc7987..74b9e10c530d 100644 --- a/system/HTTP/ResponseTrait.php +++ b/system/HTTP/ResponseTrait.php @@ -370,7 +370,7 @@ public function send() if ($this->CSP->enabled()) { $this->CSP->finalize($this); } else { - $this->body = str_replace(['{csp-style-nonce}', '{csp-script-nonce}'], '', $this->body ?? ''); + $this->body = $this->CSP->clearNoncePlaceholders($this->body); } $this->sendHeaders(); diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php index af9638b6a8ba..6e3c320c1eaf 100644 --- a/tests/system/HTTP/ContentSecurityPolicyTest.php +++ b/tests/system/HTTP/ContentSecurityPolicyTest.php @@ -937,4 +937,67 @@ public function testClearDirective(): void $this->assertNotContains('report-uri http://example.com/csp/reports', $directives); $this->assertNotContains('report-to default', $directives); } + + public function testClearNoncePlaceholdersWithDefaultTags(): void + { + $config = new CSPConfig(); + $csp = new ContentSecurityPolicy($config); + + $body = 'Test {csp-script-nonce} and {csp-style-nonce} here'; + $cleaned = $csp->clearNoncePlaceholders($body); + + $this->assertSame('Test and here', $cleaned); + $this->assertStringNotContainsString('{csp-script-nonce}', $cleaned); + $this->assertStringNotContainsString('{csp-style-nonce}', $cleaned); + } + + public function testClearNoncePlaceholdersWithCustomTags(): void + { + $config = new CSPConfig(); + $config->scriptNonceTag = '{custom-script-nonce}'; + $config->styleNonceTag = '{custom-style-nonce}'; + $csp = new ContentSecurityPolicy($config); + + $body = 'Test {custom-script-nonce} and {custom-style-nonce} here'; + $cleaned = $csp->clearNoncePlaceholders($body); + + $this->assertSame('Test and here', $cleaned); + $this->assertStringNotContainsString('{custom-script-nonce}', $cleaned); + $this->assertStringNotContainsString('{custom-style-nonce}', $cleaned); + } + + public function testClearNoncePlaceholdersWithEmptyBody(): void + { + $config = new CSPConfig(); + $csp = new ContentSecurityPolicy($config); + + $body = ''; + $cleaned = $csp->clearNoncePlaceholders($body); + + $this->assertSame('', $cleaned); + } + + public function testClearNoncePlaceholdersWithNoPlaceholders(): void + { + $config = new CSPConfig(); + $csp = new ContentSecurityPolicy($config); + + $body = 'Test body with no placeholders'; + $cleaned = $csp->clearNoncePlaceholders($body); + + $this->assertSame($body, $cleaned); + } + + public function testClearNoncePlaceholdersWithMultiplePlaceholders(): void + { + $config = new CSPConfig(); + $csp = new ContentSecurityPolicy($config); + + $body = ''; + $cleaned = $csp->clearNoncePlaceholders($body); + + $this->assertStringNotContainsString('{csp-script-nonce}', $cleaned); + $this->assertStringNotContainsString('{csp-style-nonce}', $cleaned); + $this->assertSame('', $cleaned); + } } diff --git a/tests/system/HTTP/ResponseTest.php b/tests/system/HTTP/ResponseTest.php index 84408df0e5a5..9ee3612da492 100644 --- a/tests/system/HTTP/ResponseTest.php +++ b/tests/system/HTTP/ResponseTest.php @@ -577,4 +577,80 @@ public function testPretendOutput(): void $this->assertSame('Happy days', $actual); } + + public function testSendRemovesDefaultNoncePlaceholdersWhenCSPDisabled(): void + { + $config = new App(); + $config->CSPEnabled = false; + + $response = new Response($config); + $response->pretend(true); + + $body = ''; + $response->setBody($body); + + ob_start(); + $response->send(); + $actual = ob_get_contents(); + ob_end_clean(); + + // Nonce placeholders should be removed when CSP is disabled + $this->assertStringNotContainsString('{csp-script-nonce}', $actual); + $this->assertStringNotContainsString('{csp-style-nonce}', $actual); + $this->assertStringContainsString('', $actual); + $this->assertStringContainsString('', $actual); + } + + public function testSendRemovesCustomNoncePlaceholdersWhenCSPDisabled(): void + { + $appConfig = new App(); + $appConfig->CSPEnabled = false; + + // Create custom CSP config with custom nonce tags + $cspConfig = new \Config\ContentSecurityPolicy(); + $cspConfig->scriptNonceTag = '{custom-script-tag}'; + $cspConfig->styleNonceTag = '{custom-style-tag}'; + + $response = new Response($appConfig); + $response->pretend(true); + + // Inject the custom CSP config + $reflection = new \ReflectionClass($response); + $cspProperty = $reflection->getProperty('CSP'); + $cspProperty->setValue($response, new ContentSecurityPolicy($cspConfig)); + + $body = ''; + $response->setBody($body); + + ob_start(); + $response->send(); + $actual = ob_get_contents(); + ob_end_clean(); + + // Custom nonce placeholders should be removed when CSP is disabled + $this->assertStringNotContainsString('{custom-script-tag}', $actual); + $this->assertStringNotContainsString('{custom-style-tag}', $actual); + $this->assertStringContainsString('', $actual); + $this->assertStringContainsString('', $actual); + } + + public function testSendWithCSPDisabledDoesNotAffectBodyWithoutNonceTags(): void + { + $config = new App(); + $config->CSPEnabled = false; + + $response = new Response($config); + $response->pretend(true); + + $body = ''; + $response->setBody($body); + + ob_start(); + $response->send(); + $actual = ob_get_contents(); + ob_end_clean(); + + // Body without nonce tags should remain unchanged + $this->assertSame($body, $actual); + } }