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('/