From e11a0067e0dc7bf2e467916b2d5b34544b5b2f4a Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 19 Jun 2026 00:16:11 -0400 Subject: [PATCH 1/4] Fix deserialization of untrusted data weakness The unserialize filter has a weakness to arbitrary class usage which can be combined with user input to create unserialization gadgets which are used in RCE vulnerability chains. I've also chosen to deprecate these functions. I see no reason to continue having them when they have so many sharp edges. Applications relying on these features, can add their own filters. Thanks to Volker Dusch and the PHP Ecosystem security team for reporting this. --- src/Twig/Extension/UtilsExtension.php | 13 ++++- .../Twig/Extension/UtilsExtensionTest.php | 53 +++++++++++++++++++ tests/test_app/src/GadgetMarker.php | 15 ++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/TestCase/Twig/Extension/UtilsExtensionTest.php create mode 100644 tests/test_app/src/GadgetMarker.php diff --git a/src/Twig/Extension/UtilsExtension.php b/src/Twig/Extension/UtilsExtension.php index d02bbab..606d37d 100644 --- a/src/Twig/Extension/UtilsExtension.php +++ b/src/Twig/Extension/UtilsExtension.php @@ -20,6 +20,7 @@ use Twig\Extension\AbstractExtension; use Twig\TwigFilter; +use function Cake\Core\deprecationWarning; /** * Class UtilsExtension. @@ -34,8 +35,16 @@ class UtilsExtension extends AbstractExtension public function getFilters(): array { return [ - new TwigFilter('serialize', 'serialize'), - new TwigFilter('unserialize', 'unserialize'), + new TwigFilter('serialize', function (string $value): mixed { + deprecationWarning('5.0.2', 'Usage of serialize in templates deprecated.'); + + return serialize($value); + }), + new TwigFilter('unserialize', function (string $value): mixed { + deprecationWarning('5.0.2', 'unserialize is deprecated. Its usage creates arbitrary object deserialization issues'); + + return unserialize($value, ['allowed_classes' => false]); + }), new TwigFilter('md5', 'md5'), new TwigFilter('base64_encode', 'base64_encode'), new TwigFilter('base64_decode', 'base64_decode'), diff --git a/tests/TestCase/Twig/Extension/UtilsExtensionTest.php b/tests/TestCase/Twig/Extension/UtilsExtensionTest.php new file mode 100644 index 0000000..f363981 --- /dev/null +++ b/tests/TestCase/Twig/Extension/UtilsExtensionTest.php @@ -0,0 +1,53 @@ +extension = new UtilsExtension(); + } + + public function testUnserializePreventObject(): void + { + $twig = new Environment(new ArrayLoader([ + // {% set %} so we exercise the filter without stringifying the result. + 'object' => '{% set _ = payload|unserialize %}(rendered)', + 'array' => '{{ (payload|unserialize)["role"] }}', + ])); + $twig->addExtension(new UtilsExtension()); + + // 1) Object payload: does a gadget's magic method run? + GadgetMarker::$woken = false; + $this->deprecated(function () use ($twig) { + $twig->render('object', ['payload' => serialize(new GadgetMarker())]); + $this->assertFalse(GadgetMarker::$woken, 'Should not have modified GadgetMarker'); + + $out = $twig->render('array', ['payload' => serialize(['role' => 'editor'])]); + $this->assertStringContainsString('editor', $out); + }); + } +} diff --git a/tests/test_app/src/GadgetMarker.php b/tests/test_app/src/GadgetMarker.php new file mode 100644 index 0000000..a159879 --- /dev/null +++ b/tests/test_app/src/GadgetMarker.php @@ -0,0 +1,15 @@ + Date: Sat, 20 Jun 2026 00:15:41 -0400 Subject: [PATCH 2/4] Fix formatting --- tests/TestCase/Twig/Extension/UtilsExtensionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/Twig/Extension/UtilsExtensionTest.php b/tests/TestCase/Twig/Extension/UtilsExtensionTest.php index f363981..b33d318 100644 --- a/tests/TestCase/Twig/Extension/UtilsExtensionTest.php +++ b/tests/TestCase/Twig/Extension/UtilsExtensionTest.php @@ -42,7 +42,7 @@ public function testUnserializePreventObject(): void // 1) Object payload: does a gadget's magic method run? GadgetMarker::$woken = false; - $this->deprecated(function () use ($twig) { + $this->deprecated(function () use ($twig): void { $twig->render('object', ['payload' => serialize(new GadgetMarker())]); $this->assertFalse(GadgetMarker::$woken, 'Should not have modified GadgetMarker'); From 57c213f0e8471f28a96812dff5d9ba31e1f14997 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 20 Jun 2026 00:19:16 -0400 Subject: [PATCH 3/4] Add skip for php 8.2 --- tests/TestCase/Twig/Extension/UtilsExtensionTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/TestCase/Twig/Extension/UtilsExtensionTest.php b/tests/TestCase/Twig/Extension/UtilsExtensionTest.php index b33d318..f268bb6 100644 --- a/tests/TestCase/Twig/Extension/UtilsExtensionTest.php +++ b/tests/TestCase/Twig/Extension/UtilsExtensionTest.php @@ -33,6 +33,8 @@ protected function setUp(): void public function testUnserializePreventObject(): void { + $this->skipIf(version_compare(PHP_VERSION, '8.3.0', '<'), 'Requires PHP8.3 or higher'); + $twig = new Environment(new ArrayLoader([ // {% set %} so we exercise the filter without stringifying the result. 'object' => '{% set _ = payload|unserialize %}(rendered)', From e4c5e0960342290a3a435290c779aa57cadef083 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 20 Jun 2026 00:21:23 -0400 Subject: [PATCH 4/4] Apply rector fixes --- tests/TestCase/Twig/Extension/UtilsExtensionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/Twig/Extension/UtilsExtensionTest.php b/tests/TestCase/Twig/Extension/UtilsExtensionTest.php index f268bb6..f9c1784 100644 --- a/tests/TestCase/Twig/Extension/UtilsExtensionTest.php +++ b/tests/TestCase/Twig/Extension/UtilsExtensionTest.php @@ -33,7 +33,7 @@ protected function setUp(): void public function testUnserializePreventObject(): void { - $this->skipIf(version_compare(PHP_VERSION, '8.3.0', '<'), 'Requires PHP8.3 or higher'); + $this->skipIf(PHP_VERSION_ID < 80300, 'Requires PHP8.3 or higher'); $twig = new Environment(new ArrayLoader([ // {% set %} so we exercise the filter without stringifying the result.