Skip to content

Bug: CSRF filter URL-encodes JSON request body in 4.7.1, breaking IncomingRequest::getJSON() #10063

@adnduweb

Description

@adnduweb

PHP Version

8.4

CodeIgniter4 Version

7.4.1

CodeIgniter4 Installation Method

Composer (as dependency to an existing project)

Which operating systems have you tested for this bug?

macOS

Which server did you use?

fpm-fcgi

Environment

development

Database

MySQL 5.6

What happened?

After upgrading from CI4 4.7.0 to 4.7.1, all POST requests with Content-Type: application/json fail when calling $this->request->getJSON().
The CSRF filter modifies $request->body, URL-encoding the raw JSON string. This causes json_decode() to fail with "Failed to parse JSON string. Error: Syntax error".

Debug output proving the issue:

php://input (raw, untouched — correct):
  length: 462
  content: {"entity_type":"pages","uuid":"171354f4-..."}

$request->getBody() (after CSRF filter ran — corrupted):
  length: 643
  content: %7B%22entity_type%22%3A%22pages%22%2C%22uuid%22%3A%22171354f4-...

php://input contains valid JSON (462 bytes).
$request->getBody() returns the same data but URL-encoded (643 bytes), ending with =.
This indicates the CSRF filter treats the JSON body as a form field key and runs it through URL encoding (likely via parse_str() or similar).
This worked correctly in 4.7.0 — the CSRF filter did not alter the body of application/json requests.

Steps to Reproduce

Fresh CI4 4.7.1 install (or upgrade from 4.7.0)
Enable CSRF filter globally in app/Config/Filters.php:

public array $globals = [
    'before' => [
        'csrf',
    ],
];

Create a simple controller:

<?php

namespace App\Controllers;

use CodeIgniter\Controller;

class ApiTest extends Controller
{
    public function save()
    {
        $rawInput = file_get_contents('php://input');
        $body     = $this->request->getBody();

        log_message('debug', 'php://input length: ' . strlen($rawInput));
        log_message('debug', 'getBody() length: '   . strlen($body ?? ''));
        log_message('debug', 'php://input first 100: ' . substr($rawInput, 0, 100));
        log_message('debug', 'getBody() first 100: '   . substr($body ?? '', 0, 100));

        // Throws: "Failed to parse JSON string. Error: Syntax error"
        $data = $this->request->getJSON(true);

        return $this->response->setJSON(['success' => true, 'data' => $data]);
    }
}

Add route:

$routes->post('api/test', 'ApiTest::save');

Send a JSON POST request with a valid CSRF token in the header:

fetch('/api/test', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-TOKEN': csrfToken,
        'X-Requested-With': 'XMLHttpRequest'
    },
    body: JSON.stringify({ name: 'test', value: 'hello world' })
});

Result: 500 error — Failed to parse JSON string. Error: Syntax error
Logs confirm: getBody() returns a URL-encoded string instead of raw JSON.

Expected Output

$this->request->getJSON() should return the decoded JSON object, identical to:
json_decode(file_get_contents('php://input'));
The CSRF filter should not modify $request->body when the Content-Type is application/json. The CSRF token should be read from the X-CSRF-TOKEN header — which is the standard approach for AJAX/JSON requests — without altering the request body.
In 4.7.0, this worked correctly: the body was preserved as raw JSON after the CSRF filter ran.

Anything else?

No response

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