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/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