Skip to content

Bug: CSP nonce replacement corrupts JSON responses in AJAX requests #9934

@patel-vansh

Description

@patel-vansh

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

  1. Enable CSP in your application:

    // app/Config/ContentSecurityPolicy.php
    public bool $CSPEnabled = true;
  2. 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
        }
    }
  3. 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
    });
  4. 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:

  1. Only replace nonce placeholders in HTML responses (Content-Type: text/html)
  2. Not replace or corrupt nonce placeholders in JSON, XML, or other structured data responses
  3. 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);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugVerified issues on the current code behavior or pull requests that will fix them

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions