From 88759af55f9609c77687cf7f25f13c5982f078b0 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 12 Oct 2026 22:13:32 +0200 Subject: [PATCH] fix: handle invalid values and empty tag names in XMLFormatter --- system/Format/Exceptions/FormatException.php | 11 +++++ system/Format/XMLFormatter.php | 11 ++++- system/Language/en/Format.php | 1 + tests/system/Format/XMLFormatterTest.php | 48 ++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.6.rst | 7 +++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php index cde333b1292c..7c7e2168c69a 100644 --- a/system/Format/Exceptions/FormatException.php +++ b/system/Format/Exceptions/FormatException.php @@ -57,6 +57,17 @@ public static function forInvalidMime(string $mime) return new static(lang('Format.invalidMime', [$mime])); } + /** + * Thrown in XMLFormatter when a value cannot be converted + * to a string. + * + * @return static + */ + public static function forInvalidXMLValue(string $type) + { + return new static(lang('Format.invalidXMLValue', [$type])); + } + /** * Thrown on XMLFormatter when the `simplexml` extension * is not installed. diff --git a/system/Format/XMLFormatter.php b/system/Format/XMLFormatter.php index 5ef2908858b7..3a2e8a6f20a9 100644 --- a/system/Format/XMLFormatter.php +++ b/system/Format/XMLFormatter.php @@ -16,6 +16,7 @@ use CodeIgniter\Format\Exceptions\FormatException; use Config\Format; use SimpleXMLElement; +use Stringable; /** * XML data formatter @@ -68,7 +69,11 @@ protected function arrayToXML(array $data, &$output) $subnode = $output->addChild("{$key}"); $this->arrayToXML($value, $subnode); } else { - $output->addChild("{$key}", htmlspecialchars("{$value}")); + if (is_object($value) && ! $value instanceof Stringable) { + throw FormatException::forInvalidXMLValue($value::class); + } + + $output->addChild("{$key}", htmlspecialchars((string) $value)); } } } @@ -98,6 +103,8 @@ protected function normalizeXMLTag($key) $key = preg_replace("/[^{$validName}-]+/u", '', $key); $key = preg_replace("/^[^{$startChar}]+/u", 'item$0', $key); - return preg_replace('/^(xml).*/iu', 'item$0', $key); // XML is a reserved starting word + $key = preg_replace('/^(xml).*/iu', 'item$0', $key); // XML is a reserved starting word + + return $key === '' ? 'item' : $key; } } diff --git a/system/Language/en/Format.php b/system/Language/en/Format.php index 7d792e2b6128..1f217798afe3 100644 --- a/system/Language/en/Format.php +++ b/system/Language/en/Format.php @@ -16,5 +16,6 @@ 'invalidFormatter' => '"{0}" is not a valid Formatter class.', 'invalidJSON' => 'Failed to parse JSON string. Error: {0}', 'invalidMime' => 'No Formatter defined for mime type: "{0}".', + 'invalidXMLValue' => 'Cannot convert the value of type "{0}" to a string for XML output.', 'missingExtension' => 'The SimpleXML extension is required to format XML.', ]; diff --git a/tests/system/Format/XMLFormatterTest.php b/tests/system/Format/XMLFormatterTest.php index 107418a5d68c..f86aa95d1feb 100644 --- a/tests/system/Format/XMLFormatterTest.php +++ b/tests/system/Format/XMLFormatterTest.php @@ -13,10 +13,13 @@ namespace CodeIgniter\Format; +use CodeIgniter\Format\Exceptions\FormatException; use CodeIgniter\Test\CIUnitTestCase; use DOMDocument; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use stdClass; +use Stringable; /** * @internal @@ -152,6 +155,51 @@ public static function provideValidatingInvalidTags(): iterable ]; } + public function testEmptyTagNameAfterSanitization(): void + { + $data = [ + '!@#' => 'value', + ]; + + $expected = <<<'EOH' + + value + + EOH; + + $this->assertSame($expected, $this->xmlFormatter->format($data)); + } + + public function testStringableObjectValue(): void + { + $value = new class () implements Stringable { + public function __toString(): string + { + return 'bar'; + } + }; + + $data = [ + 'foo' => $value, + ]; + + $expected = <<<'EOH' + + bar + + EOH; + + $this->assertSame($expected, $this->xmlFormatter->format($data)); + } + + public function testNonStringableObjectValueThrows(): void + { + $this->expectException(FormatException::class); + $this->expectExceptionMessage('Cannot convert the value of type "stdClass" to a string for XML output.'); + + $this->xmlFormatter->format(['foo' => new stdClass()]); + } + public function testDeepNestedArrayToXml(): void { $data = [ diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst index 360221806c4f..029fa79286e5 100644 --- a/user_guide_src/source/changelogs/v4.7.6.rst +++ b/user_guide_src/source/changelogs/v4.7.6.rst @@ -18,6 +18,8 @@ BREAKING Message Changes *************** +- Added the ``Format.invalidXMLValue`` language string. + ******* Changes ******* @@ -31,6 +33,11 @@ Bugs Fixed ********** - **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings. +- **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after + sanitization no longer triggers a ``ValueError``; the fallback tag name ``item`` + is used instead. Values that cannot be converted to a string, such as objects + not implementing ``Stringable``, now throw a ``FormatException`` instead of a + fatal ``Error``. - **Security:** Fixed a bug where a valid JSON request body that is not an object (for example a JSON array) was corrupted after CSRF verification when the token was sent via the ``X-CSRF-TOKEN`` header.