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