diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe2903158fa..efb902c3dcdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [v4.7.2](https://github.com/codeigniter4/CodeIgniter4/tree/v4.7.2) (2026-03-24) +[Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.7.1...v4.7.2) + +### Fixed Bugs + +* fix: preserve JSON body when CSRF token is sent in header by @michalsn in https://github.com/codeigniter4/CodeIgniter4/pull/10064 + ## [v4.7.1](https://github.com/codeigniter4/CodeIgniter4/tree/v4.7.1) (2026-03-22) [Full Changelog](https://github.com/codeigniter4/CodeIgniter4/compare/v4.7.0...v4.7.1) diff --git a/admin/create-new-changelog.php b/admin/create-new-changelog.php index eca1696676b2..32ac5fdaa52c 100644 --- a/admin/create-new-changelog.php +++ b/admin/create-new-changelog.php @@ -92,6 +92,7 @@ function replace_file_content(string $path, string $pattern, string $replace): v ); if (! in_array('--dry-run', $argv, true)) { + system('git add ./system/CodeIgniter.php'); system("git add {$newChangelog} {$changelogIndex}"); system("git add {$newUpgrading} {$upgradingIndex}"); system("git commit -m \"docs: add changelog and upgrade for v{$newVersion}\""); diff --git a/phpdoc.dist.xml b/phpdoc.dist.xml index 4c589ae8c5b0..f5a5f247f456 100644 --- a/phpdoc.dist.xml +++ b/phpdoc.dist.xml @@ -10,7 +10,7 @@ api/build/ api/cache/ - + system diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index c29efc7781f8..3e60ebdfb49d 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -55,7 +55,7 @@ class CodeIgniter /** * The current version of CodeIgniter Framework */ - public const CI_VERSION = '4.7.1'; + public const CI_VERSION = '4.7.2'; /** * App startup time. diff --git a/system/Security/Security.php b/system/Security/Security.php index 873fee7469a8..4ac0de3f8ff8 100644 --- a/system/Security/Security.php +++ b/system/Security/Security.php @@ -298,15 +298,18 @@ private function removeTokenInRequest(IncomingRequest $request): void $json = null; } - if (is_object($json) && property_exists($json, $tokenName)) { - unset($json->{$tokenName}); - $request->setBody(json_encode($json)); + if (is_object($json)) { + if (property_exists($json, $tokenName)) { + unset($json->{$tokenName}); + $request->setBody(json_encode($json)); + } return; } // If the token is found in form-encoded data, we can safely remove it. parse_str($body, $result); + unset($result[$tokenName]); $request->setBody(http_build_query($result)); } diff --git a/tests/system/Security/SecurityCSRFSessionTest.php b/tests/system/Security/SecurityCSRFSessionTest.php index 60eb30aa9931..5c7aaf336e1f 100644 --- a/tests/system/Security/SecurityCSRFSessionTest.php +++ b/tests/system/Security/SecurityCSRFSessionTest.php @@ -251,6 +251,37 @@ public function testCSRFVerifyJsonReturnsSelfOnMatch(): void $this->assertSame('{"foo":"bar"}', $request->getBody()); } + public function testCSRFVerifyHeaderWithJsonBodyPreservesBody(): void + { + service('superglobals')->setServer('REQUEST_METHOD', 'POST'); + + $request = $this->createIncomingRequest(); + $body = '{"foo":"bar"}'; + + $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); + $request->setBody($body); + $security = $this->createSecurity(); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); + $this->assertSame($body, $request->getBody()); + } + + public function testCSRFVerifyHeaderWithJsonBodyStripsTokenFromBody(): void + { + service('superglobals')->setServer('REQUEST_METHOD', 'POST'); + + $request = $this->createIncomingRequest(); + + $request->setHeader('X-CSRF-TOKEN', '8b9218a55906f9dcc1dc263dce7f005a'); + $request->setBody('{"csrf_test_name":"8b9218a55906f9dcc1dc263dce7f005a","foo":"bar"}'); + $security = $this->createSecurity(); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertLogged('info', 'CSRF token verified.'); + $this->assertSame('{"foo":"bar"}', $request->getBody()); + } + public function testRegenerateWithFalseSecurityRegenerateProperty(): void { service('superglobals') diff --git a/tests/system/Security/SecurityTest.php b/tests/system/Security/SecurityTest.php index 90f2139b2ccd..932dfc0df2c0 100644 --- a/tests/system/Security/SecurityTest.php +++ b/tests/system/Security/SecurityTest.php @@ -204,6 +204,39 @@ public function testCsrfVerifyJsonReturnsSelfOnMatch(): void $this->assertSame('{"foo":"bar"}', $request->getBody()); } + public function testCsrfVerifyHeaderWithJsonBodyPreservesBody(): void + { + service('superglobals') + ->setServer('REQUEST_METHOD', 'POST') + ->setCookie('csrf_cookie_name', self::CORRECT_CSRF_HASH); + + $security = $this->createMockSecurity(); + $request = $this->createIncomingRequest(); + $body = '{"foo":"bar"}'; + + $request->setHeader('X-CSRF-TOKEN', self::CORRECT_CSRF_HASH); + $request->setBody($body); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertSame($body, $request->getBody()); + } + + public function testCsrfVerifyHeaderWithJsonBodyStripsTokenFromBody(): void + { + service('superglobals') + ->setServer('REQUEST_METHOD', 'POST') + ->setCookie('csrf_cookie_name', self::CORRECT_CSRF_HASH); + + $security = $this->createMockSecurity(); + $request = $this->createIncomingRequest(); + + $request->setHeader('X-CSRF-TOKEN', self::CORRECT_CSRF_HASH); + $request->setBody('{"csrf_test_name":"' . self::CORRECT_CSRF_HASH . '","foo":"bar"}'); + + $this->assertInstanceOf(Security::class, $security->verify($request)); + $this->assertSame('{"foo":"bar"}', $request->getBody()); + } + public function testCsrfVerifyPutBodyThrowsExceptionOnNoMatch(): void { service('superglobals') diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst index 6f2dc8c45a0a..ca80566db7b3 100644 --- a/user_guide_src/source/changelogs/index.rst +++ b/user_guide_src/source/changelogs/index.rst @@ -12,6 +12,7 @@ See all the changes. .. toctree:: :titlesonly: + v4.7.2 v4.7.1 v4.7.0 v4.6.5 diff --git a/user_guide_src/source/changelogs/v4.7.2.rst b/user_guide_src/source/changelogs/v4.7.2.rst new file mode 100644 index 000000000000..c202c52637eb --- /dev/null +++ b/user_guide_src/source/changelogs/v4.7.2.rst @@ -0,0 +1,23 @@ +############# +Version 4.7.2 +############# + +Release Date: March 24, 2026 + +**4.7.2 release of CodeIgniter4** + +.. contents:: + :local: + :depth: 3 + +********** +Bugs Fixed +********** + +- **Security:** Fixed a bug where the CSRF filter could corrupt JSON request bodies after successful + verification when the CSRF token was provided via the ``X-CSRF-TOKEN`` header. + This caused ``IncomingRequest::getJSON()`` to fail on valid ``application/json`` requests. + +See the repo's +`CHANGELOG.md `_ +for a complete list of bugs fixed. diff --git a/user_guide_src/source/conf.py b/user_guide_src/source/conf.py index 7f8bfc3b3f75..7fa5ade71d7e 100644 --- a/user_guide_src/source/conf.py +++ b/user_guide_src/source/conf.py @@ -26,7 +26,7 @@ version = '4.7' # The full version, including alpha/beta/rc tags. -release = '4.7.1' +release = '4.7.2' # -- General configuration --------------------------------------------------- diff --git a/user_guide_src/source/installation/upgrade_472.rst b/user_guide_src/source/installation/upgrade_472.rst new file mode 100644 index 000000000000..92498b8d0878 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_472.rst @@ -0,0 +1,43 @@ +############################# +Upgrading from 4.7.1 to 4.7.2 +############################# + +Please refer to the upgrade instructions corresponding to your installation method. + +- :ref:`Composer Installation App Starter Upgrading ` +- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading ` +- :ref:`Manual Installation Upgrading ` + +.. contents:: + :local: + :depth: 2 + +************* +Project Files +************* + +Some files in the **project space** (root, app, public, writable) received updates. Due to +these files being outside of the **system** scope they will not be changed without your intervention. + +.. note:: There are some third-party CodeIgniter modules available to assist + with merging changes to the project space: + `Explore on Packagist `_. + +Content Changes +=============== + +The following files received significant changes (including deprecations or visual adjustments) +and it is recommended that you merge the updated versions with your application: + +Config +------ + +- No config files were changed in this release. + +All Changes +=========== + +This is a list of all files in the **project space** that received changes; +many will be simple comments or formatting that have no effect on the runtime: + +- No project files were changed in this release. diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index b06f9f62a68f..48daf40f482b 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -22,6 +22,7 @@ Alternatively, replace it with a new file and add your previous lines. backward_compatibility_notes + upgrade_472 upgrade_471 upgrade_470 upgrade_465