-
Notifications
You must be signed in to change notification settings - Fork 2k
Description
PHP Version
8.2, 8.3, 8.4
CodeIgniter4 Version
All versions with CSP support
CodeIgniter4 Installation Method
Composer (using codeigniter4/appstarter)
Which operating systems have you tested for this bug?
macOS
Which server did you use?
apache
Environment
development
Database
No response
What happened?
When Content Security Policy (CSP) is enabled and an exception occurs during an AJAX request, the JSON error response becomes corrupted and causes JSON parsing errors on the client side.
The response contains malformed JSON with improperly escaped quotes, looking something like:
{"script-nonce-tag":"nonce="ads45adf4g2154df""}This results in JavaScript JSON.parse() errors like:
- SyntaxError: Unexpected token n in JSON at position X
- JSON Parse error: Unrecognized token
The issue occurs when:
- CSP is enabled in the application (
$CSPEnabled = true) - An exception is thrown during an AJAX request (with Accept: application/json header)
- The error response contains CSP nonce placeholder tags (e.g., from debug output or error traces)
- The CSP generateNonces() method replaces these placeholders inside the JSON string values, breaking the JSON format
Steps to Reproduce
-
Enable CSP in your application:
// app/Config/ContentSecurityPolicy.php public bool $CSPEnabled = true;
-
Create a controller that throws an exception during AJAX:
// app/Controllers/Test.php namespace App\Controllers; class Test extends BaseController { public function ajaxError() { // This will trigger an exception $array = []; echo $array['undefined_key']; // Undefined array key error } }
-
Make an AJAX request to the endpoint:
fetch('/test/ajaxError', { headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => response.json()) .catch(error => { console.error('JSON Parse Error:', error); // This error will be thrown due to malformed JSON });
-
Observe the error:
- The browser console shows a JSON parsing error
- Inspecting the raw response shows corrupted JSON with
nonce="..."inside string values - The response body contains something like:
{"error":"...","trace":"...nonce=\"abc123\"..."}
Expected Output
The JSON error response should remain valid JSON that can be properly parsed by the client.
Expected behavior:
{
"error": "Undefined array key",
"message": "...",
"trace": [
"at line 10 in file.php",
"..."
]
}The CSP nonce replacement logic should:
- Only replace nonce placeholders in HTML responses (
Content-Type: text/html) - Not replace or corrupt nonce placeholders in JSON, XML, or other structured data responses
- Either remove the placeholders or leave them as-is in non-HTML responses
Anything else?
Root Cause
The bug is in system/HTTP/ContentSecurityPolicy.php in the generateNonces() method (around line 907).
The method performs a blind regex replacement of {csp-script-nonce} and {csp-style-nonce} placeholders on all response bodies, regardless of the Content-Type. This breaks JSON and XML responses.
// Current buggy code
protected function generateNonces(ResponseInterface $response)
{
$body = (string) $response->getBody();
if ($body === '') {
return;
}
// This replaces nonces in ALL content types, including JSON
$pattern = sprintf('/(%s|%s)/', preg_quote($this->styleNonceTag, '/'), preg_quote($this->scriptNonceTag, '/'));
$body = preg_replace_callback($pattern, function ($match): string {
$nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
return "nonce=\"{$nonce}\""; // This breaks JSON!
}, $body);
$response->setBody($body);
}Suggested Fix
Add a Content-Type check to only perform nonce replacement for HTML responses:
protected function generateNonces(ResponseInterface $response)
{
$body = (string) $response->getBody();
if ($body === '') {
return;
}
// Check Content-Type header
$contentType = $response->getHeaderLine('Content-Type');
// Only replace nonces in HTML content, not JSON/XML/etc
if ($contentType !== '' && ! str_contains($contentType, 'text/html')) {
// For non-HTML responses, just remove the placeholders
$body = str_replace([$this->styleNonceTag, $this->scriptNonceTag], '', $body);
$response->setBody($body);
return;
}
// For HTML: replace placeholders with actual nonces (existing behavior)
$pattern = sprintf('/(%s|%s)/', preg_quote($this->styleNonceTag, '/'), preg_quote($this->scriptNonceTag, '/'));
$body = preg_replace_callback($pattern, function ($match): string {
$nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
return "nonce=\"{$nonce}\"";
}, $body);
$response->setBody($body);
}