diff --git a/system/HTTP/ContentSecurityPolicy.php b/system/HTTP/ContentSecurityPolicy.php
index 04d93365af25..f15a85df32d9 100644
--- a/system/HTTP/ContentSecurityPolicy.php
+++ b/system/HTTP/ContentSecurityPolicy.php
@@ -898,13 +898,17 @@ protected function generateNonces(ResponseInterface $response)
return;
}
+ // Escape quotes for JSON responses to prevent corrupting the JSON body
+ $jsonEscape = str_contains($response->getHeaderLine('Content-Type'), 'json');
+
// Replace style and script placeholders with nonces
$pattern = sprintf('/(%s|%s)/', preg_quote($this->styleNonceTag, '/'), preg_quote($this->scriptNonceTag, '/'));
- $body = preg_replace_callback($pattern, function ($match): string {
+ $body = preg_replace_callback($pattern, function ($match) use ($jsonEscape): string {
$nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
+ $attr = 'nonce="' . $nonce . '"';
- return "nonce=\"{$nonce}\"";
+ return $jsonEscape ? str_replace('"', '\\"', $attr) : $attr;
}, $body);
$response->setBody($body);
diff --git a/tests/system/Debug/ExceptionHandlerTest.php b/tests/system/Debug/ExceptionHandlerTest.php
index a1958be5f36b..7bd7bdb35465 100644
--- a/tests/system/Debug/ExceptionHandlerTest.php
+++ b/tests/system/Debug/ExceptionHandlerTest.php
@@ -40,6 +40,8 @@ protected function setUp(): void
parent::setUp();
$this->handler = new ExceptionHandler(new ExceptionsConfig());
+
+ $this->resetServices();
}
public function testDetermineViewsPageNotFoundException(): void
diff --git a/tests/system/HTTP/ContentSecurityPolicyTest.php b/tests/system/HTTP/ContentSecurityPolicyTest.php
index af9638b6a8ba..beafa54ce164 100644
--- a/tests/system/HTTP/ContentSecurityPolicyTest.php
+++ b/tests/system/HTTP/ContentSecurityPolicyTest.php
@@ -937,4 +937,40 @@ public function testClearDirective(): void
$this->assertNotContains('report-uri http://example.com/csp/reports', $directives);
$this->assertNotContains('report-to default', $directives);
}
+
+ #[PreserveGlobalState(false)]
+ #[RunInSeparateProcess]
+ public function testGenerateNoncesReplacesPlaceholdersInHtml(): void
+ {
+ $body = '';
+
+ $this->response->setBody($body);
+ $this->csp->finalize($this->response);
+
+ $result = $this->response->getBody();
+
+ $this->assertMatchesRegularExpression('/