Skip to content

Commit e011848

Browse files
committed
fix: escape CSP nonce attributes in JSON responses
1 parent 044a71c commit e011848

File tree

4 files changed

+75
-2
lines changed

4 files changed

+75
-2
lines changed

system/HTTP/ContentSecurityPolicy.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -898,13 +898,17 @@ protected function generateNonces(ResponseInterface $response)
898898
return;
899899
}
900900

901+
// Escape quotes for JSON responses to prevent corrupting the JSON body
902+
$jsonEscape = str_contains($response->getHeaderLine('Content-Type'), 'json');
903+
901904
// Replace style and script placeholders with nonces
902905
$pattern = sprintf('/(%s|%s)/', preg_quote($this->styleNonceTag, '/'), preg_quote($this->scriptNonceTag, '/'));
903906

904-
$body = preg_replace_callback($pattern, function ($match): string {
907+
$body = preg_replace_callback($pattern, function ($match) use ($jsonEscape): string {
905908
$nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
909+
$attr = 'nonce="' . $nonce . '"';
906910

907-
return "nonce=\"{$nonce}\"";
911+
return $jsonEscape ? str_replace('"', '\\"', $attr) : $attr;
908912
}, $body);
909913

910914
$response->setBody($body);

tests/system/Debug/ExceptionHandlerTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
use App\Controllers\Home;
1717
use CodeIgniter\Exceptions\PageNotFoundException;
1818
use CodeIgniter\Exceptions\RuntimeException;
19+
use CodeIgniter\HTTP\IncomingRequest;
20+
use CodeIgniter\HTTP\Response;
1921
use CodeIgniter\Test\CIUnitTestCase;
2022
use CodeIgniter\Test\IniTestTrait;
2123
use CodeIgniter\Test\StreamFilterTrait;
24+
use Config\App;
2225
use Config\Exceptions as ExceptionsConfig;
2326
use Config\Services;
2427
use PHPUnit\Framework\Attributes\Group;
@@ -40,6 +43,8 @@ protected function setUp(): void
4043
parent::setUp();
4144

4245
$this->handler = new ExceptionHandler(new ExceptionsConfig());
46+
47+
$this->resetServices();
4348
}
4449

4550
public function testDetermineViewsPageNotFoundException(): void
@@ -386,4 +391,32 @@ public function testSanitizeDataWithScalars(): void
386391
$this->assertFalse($sanitizeData(false));
387392
$this->assertNull($sanitizeData(null));
388393
}
394+
395+
public function testHandleJsonResponseWithCSPEnabledProducesValidJson(): void
396+
{
397+
$config = config(App::class);
398+
$config->CSPEnabled = true;
399+
400+
/** @var IncomingRequest $request */
401+
$request = service('incomingrequest', $config, false);
402+
$request->setHeader('accept', 'application/json');
403+
$response = new Response($config);
404+
$response->pretend();
405+
406+
$exception = new RuntimeException('Test exception');
407+
408+
ob_start();
409+
$this->handler->handle($exception, $request, $response, 500, EXIT_ERROR);
410+
$output = ob_get_clean();
411+
412+
$json = json_decode($output);
413+
$this->assertNotNull($json);
414+
415+
// Nonce placeholders must not appear in the output
416+
$this->assertStringNotContainsString('{csp-style-nonce}', (string) $output);
417+
$this->assertStringNotContainsString('{csp-script-nonce}', (string) $output);
418+
419+
// The nonce attribute values should be properly JSON-escaped
420+
$this->assertMatchesRegularExpression('/nonce=\\\\"[A-Za-z0-9+\/=]+\\\\"/', $output);
421+
}
389422
}

tests/system/HTTP/ContentSecurityPolicyTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,4 +937,38 @@ public function testClearDirective(): void
937937
$this->assertNotContains('report-uri http://example.com/csp/reports', $directives);
938938
$this->assertNotContains('report-to default', $directives);
939939
}
940+
941+
#[PreserveGlobalState(false)]
942+
#[RunInSeparateProcess]
943+
public function testGenerateNoncesReplacesPlaceholdersInHtml(): void
944+
{
945+
$body = '<style {csp-style-nonce}>body{}</style><script {csp-script-nonce}>alert(1)</script>';
946+
947+
$this->response->setBody($body);
948+
$this->csp->finalize($this->response);
949+
950+
$result = $this->response->getBody();
951+
952+
$this->assertMatchesRegularExpression('/<style nonce="[A-Za-z0-9+\/=]+">/', $result);
953+
$this->assertMatchesRegularExpression('/<script nonce="[A-Za-z0-9+\/=]+">/', $result);
954+
$this->assertStringNotContainsString('{csp-style-nonce}', (string) $result);
955+
$this->assertStringNotContainsString('{csp-script-nonce}', (string) $result);
956+
}
957+
958+
#[PreserveGlobalState(false)]
959+
#[RunInSeparateProcess]
960+
public function testGenerateNoncesEscapesQuotesInJsonResponse(): void
961+
{
962+
$data = json_encode(['html' => '<script {csp-script-nonce}>alert(1)</script>']);
963+
964+
$this->response->setContentType('application/json');
965+
$this->response->setBody($data);
966+
$this->csp->finalize($this->response);
967+
968+
$result = $this->response->getBody();
969+
$parsed = json_decode($result, true);
970+
971+
$this->assertNotNull($parsed);
972+
$this->assertMatchesRegularExpression('/nonce="[A-Za-z0-9+\/=]+"/', $parsed['html']);
973+
}
940974
}

user_guide_src/source/changelogs/v4.7.1.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Deprecations
3030
Bugs Fixed
3131
**********
3232

33+
- **ContentSecurityPolicy:** Fixed a bug where ``generateNonces()`` corrupted JSON responses by replacing CSP nonce placeholders with unescaped double quotes. The method now automatically JSON-escapes nonce attributes when the response Content-Type is JSON.
34+
3335
See the repo's
3436
`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
3537
for a complete list of bugs fixed.

0 commit comments

Comments
 (0)