Improper Control of Generation of Code in Twig rendered views in shopware
Description
Shopware is an open source commerce platform based on Symfony Framework and Vue js. In a Twig environment without the Sandbox extension, it is possible to refer to PHP functions in twig filters like map, filter, sort. This allows a template to call any global PHP function and thus execute arbitrary code. The attacker must have access to a Twig environment in order to exploit this vulnerability. This problem has been fixed with 6.4.18.1 with an override of the specified filters until the integration of the Sandbox extension has been finished. Users are advised to upgrade. Users of major versions 6.1, 6.2, and 6.3 may also receive this fix via a plugin.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Twig templates in Shopware without Sandbox can call arbitrary PHP functions via map/filter/sort filters, enabling arbitrary code execution.
Vulnerability
Description
Shopware 6, an open-source commerce platform, contains a critical vulnerability in its Twig template engine component. In environments where the optional Sandbox extension is not enabled, Twig filters such as map, filter, sort, and reduce can be invoked with a string argument that names a global PHP function. This design flaw effectively allows a template to call any PHP function defined in the global scope, leading to arbitrary code execution within the application context [1][4].
Exploitation
Prerequisites
An attacker must have the ability to control or inject content into a Twig template that is rendered by the application. This typically requires authenticated access with template-editing privileges or the ability to exploit another vulnerability that introduces untrusted template content. No additional authentication bypass is needed beyond that access, and the exploit does not require a special network position apart from reachability to the web application [1][4].
Impact
Successful exploitation enables an attacker to execute arbitrary PHP code on the server. This can lead to full compromise of the Shopware instance, including data exfiltration, modification of store content, privilege escalation, and potential lateral movement within the hosting environment [4].
Mitigation
Shopware addressed the issue in version 6.4.18.1 by overriding the vulnerable Twig filters with a custom security extension that restricts which PHP functions can be called. Users of older major versions (6.1, 6.2, 6.3) can obtain a plugin providing similar protections until a full upgrade is feasible. All users are strongly advised to upgrade to the latest version or apply the available workarounds [1][3][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
shopware/platformPackagist | < 6.4.18.1 | 6.4.18.1 |
shopware/corePackagist | < 6.4.18.1 | 6.4.18.1 |
Affected products
3- ghsa-coords2 versions
< 6.4.18.1+ 1 more
- (no CPE)range: < 6.4.18.1
- (no CPE)range: < 6.4.18.1
- shopware/platformv5Range: < 6.4.18.1
Patches
189d1ea154689NEXT-24667 - Add twig improvements
11 files changed · +337 −22
changelog/_unreleased/2022-12-21-add-twig-filter-improvments.md+22 −0 added@@ -0,0 +1,22 @@ +--- +title: Add twig filter improvements +issue: NEXT-24667 +--- + +# Core + +* Added a `SecurityExtension` to allow only a whitelist of functions inside filters `map`, `filter`, `reduce` and `sort`. + +___ + +# Upgrade Information + +## Twig filter whitelist for `map`, `filter`, `reduce` and `sort` + +The whitelist can be extended using a yaml configuration: + +```yaml +shopware: + twig: + allowed_php_functions: [ "is_bool" ] +```
config-schema.json+16 −0 modified@@ -86,6 +86,9 @@ }, "profiler": { "$ref": "#/definitions/profiler" + }, + "twig": { + "$ref": "#/definitions/twig" } }, "title": "Shopware" @@ -112,6 +115,19 @@ }, "title": "Enabled profiler, available since 6.4.11.0" }, + "twig": { + "type": "object", + "additionalProperties": false, + "properties": { + "allowed_php_functions": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + } + } + }, "mail": { "type": "object", "additionalProperties": false,
phpstan-baseline.neon+1 −22 modified@@ -6875,10 +6875,7 @@ parameters: count: 1 path: src/Core/Content/Seo/SeoUrlTemplate/TemplateGroup.php - - - message: "#^Method Shopware\\\\Core\\\\Content\\\\Seo\\\\SeoUrlTwigFactory\\:\\:createTwigEnvironment\\(\\) has parameter \\$twigExtensions with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Core/Content/Seo/SeoUrlTwigFactory.php + - message: "#^Method Shopware\\\\Core\\\\Content\\\\Seo\\\\SeoUrlUpdater\\:\\:fetchLanguageChains\\(\\) has parameter \\$languages with no value type specified in iterable type array\\.$#" @@ -16925,25 +16922,7 @@ parameters: count: 1 path: src/Core/Framework/Rule/RuleConstraints.php - - - message: "#^Method Shopware\\\\Core\\\\Framework\\\\Rule\\\\ScriptRule\\:\\:render\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Core/Framework/Rule/ScriptRule.php - - - message: "#^Method Shopware\\\\Core\\\\Framework\\\\Rule\\\\ScriptRule\\:\\:setConstraints\\(\\) has parameter \\$constraints with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Core/Framework/Rule/ScriptRule.php - - - - message: "#^Property Shopware\\\\Core\\\\Framework\\\\Rule\\\\ScriptRule\\:\\:\\$constraints type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Core/Framework/Rule/ScriptRule.php - - - - message: "#^Property Shopware\\\\Core\\\\Framework\\\\Rule\\\\ScriptRule\\:\\:\\$values type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Core/Framework/Rule/ScriptRule.php - message: "#^Method Shopware\\\\Core\\\\Framework\\\\Script\\\\Api\\\\ApiHook\\:\\:__construct\\(\\) has parameter \\$request with no value type specified in iterable type array\\.$#"
src/Core/Content/Seo/SeoUrlTwigFactory.php+6 −0 modified@@ -5,20 +5,26 @@ use Cocur\Slugify\Bridge\Twig\SlugifyExtension; use Cocur\Slugify\SlugifyInterface; use Shopware\Core\Framework\Adapter\Twig\Extension\PhpSyntaxExtension; +use Shopware\Core\Framework\Adapter\Twig\SecurityExtension; use Shopware\Core\Framework\Adapter\Twig\TwigEnvironment; use Twig\Environment; use Twig\Extension\EscaperExtension; +use Twig\Extension\ExtensionInterface; use Twig\Loader\ArrayLoader; class SeoUrlTwigFactory { + /** + * @param ExtensionInterface[] $twigExtensions + */ public function createTwigEnvironment(SlugifyInterface $slugify, iterable $twigExtensions = []): Environment { $twig = new TwigEnvironment(new ArrayLoader()); $twig->setCache(false); $twig->enableStrictVariables(); $twig->addExtension(new SlugifyExtension($slugify)); $twig->addExtension(new PhpSyntaxExtension()); + $twig->addExtension(new SecurityExtension([])); /** @var EscaperExtension $coreExtension */ $coreExtension = $twig->getExtension(EscaperExtension::class);
src/Core/Framework/Adapter/Twig/SecurityExtension.php+127 −0 added@@ -0,0 +1,127 @@ +<?php declare(strict_types=1); + +namespace Shopware\Core\Framework\Adapter\Twig; + +use Twig\Extension\AbstractExtension; +use Twig\TwigFilter; + +/** + * @internal + */ +class SecurityExtension extends AbstractExtension +{ + /** + * @var array<string> + */ + private array $allowedPHPFunctions; + + /** + * @param array<string> $allowedPHPFunctions + */ + public function __construct(array $allowedPHPFunctions) + { + $this->allowedPHPFunctions = $allowedPHPFunctions; + } + + /** + * @return TwigFilter[] + */ + public function getFilters(): array + { + return [ + new TwigFilter('map', [$this, 'map']), + new TwigFilter('reduce', [$this, 'reduce']), + new TwigFilter('filter', [$this, 'filter']), + new TwigFilter('sort', [$this, 'sort']), + ]; + } + + /** + * @param iterable<mixed> $array + * @param string|callable|\Closure $function + * + * @return array<mixed> + */ + public function map(iterable $array, $function): array + { + if (\is_string($function) && !\in_array($function, $this->allowedPHPFunctions, true)) { + throw new \RuntimeException(sprintf('Function "%s" is not allowed', $function)); + } + + $result = []; + foreach ($array as $key => $value) { + // @phpstan-ignore-next-line + $result[$key] = $function($value); + } + + return $result; + } + + /** + * @param iterable<mixed> $array + * @param string|callable|\Closure $function + * @param mixed $initial + * + * @return mixed + */ + public function reduce(iterable $array, $function, $initial = null) + { + if (\is_string($function) && !\in_array($function, $this->allowedPHPFunctions, true)) { + throw new \RuntimeException(sprintf('Function "%s" is not allowed', $function)); + } + + if (!\is_array($array)) { + $array = iterator_to_array($array); + } + + // @phpstan-ignore-next-line + return array_reduce($array, $function, $initial); + } + + /** + * @param iterable<mixed> $array + * @param string|callable|\Closure $arrow + * + * @return iterable<mixed> + */ + public function filter(iterable $array, $arrow): iterable + { + if (\is_string($arrow) && !\in_array($arrow, $this->allowedPHPFunctions, true)) { + throw new \RuntimeException(sprintf('Function "%s" is not allowed', $arrow)); + } + + if (\is_array($array)) { + // @phpstan-ignore-next-line + return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + } + + // @phpstan-ignore-next-line + return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); + } + + /** + * @param iterable<mixed> $array + * @param string|callable|\Closure|null $arrow + * + * @return array<mixed> + */ + public function sort(iterable $array, $arrow = null): array + { + if (\is_string($arrow) && !\in_array($arrow, $this->allowedPHPFunctions, true)) { + throw new \RuntimeException(sprintf('Function "%s" is not allowed', $arrow)); + } + + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } + + if ($arrow !== null) { + // @phpstan-ignore-next-line + uasort($array, $arrow); + } else { + asort($array); + } + + return $array; + } +}
src/Core/Framework/DependencyInjection/Configuration.php+17 −0 modified@@ -39,6 +39,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->append($this->createCacheSection()) ->append($this->createHtmlSanitizerSection()) ->append($this->createIncrementSection()) + ->append($this->createTwigSection()) ->end(); return $treeBuilder; @@ -597,4 +598,20 @@ private function createProfilerSection(): ArrayNodeDefinition return $rootNode; } + + private function createTwigSection(): ArrayNodeDefinition + { + $treeBuilder = new TreeBuilder('twig'); + + $rootNode = $treeBuilder->getRootNode(); + $rootNode + ->children() + ->arrayNode('allowed_php_functions') + ->performNoDeepMerging() + ->scalarPrototype() + ->end() + ->end(); + + return $rootNode; + } }
src/Core/Framework/DependencyInjection/services.xml+5 −0 modified@@ -380,6 +380,11 @@ base-uri 'self'; <tag name="twig.extension"/> </service> + <service id="Shopware\Core\Framework\Adapter\Twig\SecurityExtension"> + <argument>%shopware.twig.allowed_php_functions%</argument> + <tag name="twig.extension"/> + </service> + <service id="Shopware\Core\Framework\Adapter\Twig\StringTemplateRenderer"> <argument type="service" id="twig"/> <argument>%kernel.cache_dir%%</argument>
src/Core/Framework/Resources/config/packages/shopware.yaml+3 −0 modified@@ -250,3 +250,6 @@ shopware: country_state_route: [] salutation_route: [] sitemap_route: [] + + twig: + allowed_php_functions: []
src/Core/Framework/Rule/ScriptRule.php+18 −0 modified@@ -4,6 +4,7 @@ use Shopware\Core\Framework\Adapter\Twig\Extension\ComparisonExtension; use Shopware\Core\Framework\Adapter\Twig\Extension\PhpSyntaxExtension; +use Shopware\Core\Framework\Adapter\Twig\SecurityExtension; use Shopware\Core\Framework\Adapter\Twig\TwigEnvironment; use Shopware\Core\Framework\App\Event\Hooks\AppScriptConditionHook; use Shopware\Core\Framework\Script\Debugging\Debug; @@ -24,8 +25,14 @@ class ScriptRule extends Rule { protected string $script = ''; + /** + * @var array<mixed> + */ protected array $constraints = []; + /** + * @var array<mixed> + */ protected array $values = []; protected ?\DateTimeInterface $lastModified = null; @@ -79,6 +86,8 @@ public function match(RuleScope $scope): bool $twig->addExtension(new DebugExtension()); } + $twig->addExtension(new SecurityExtension([])); + $hook = new AppScriptConditionHook($scope->getContext()); try { @@ -88,11 +97,17 @@ public function match(RuleScope $scope): bool } } + /** + * @return array<mixed> + */ public function getConstraints(): array { return $this->constraints; } + /** + * @param array<mixed> $constraints + */ public function setConstraints(array $constraints): void { $this->constraints = $constraints; @@ -103,6 +118,9 @@ public function getName(): string return 'scriptRule'; } + /** + * @param array<mixed> $context + */ private function render(TwigEnvironment $twig, Script $script, Hook $hook, string $name, array $context): bool { if (!$this->traces) {
src/Core/Framework/Script/Execution/ScriptExecutor.php+2 −0 modified@@ -5,6 +5,7 @@ use Psr\Log\LoggerInterface; use Shopware\Core\DevOps\Environment\EnvironmentHelper; use Shopware\Core\Framework\Adapter\Twig\Extension\PhpSyntaxExtension; +use Shopware\Core\Framework\Adapter\Twig\SecurityExtension; use Shopware\Core\Framework\Adapter\Twig\TwigEnvironment; use Shopware\Core\Framework\App\Event\Hooks\AppLifecycleHook; use Shopware\Core\Framework\Script\Debugging\Debug; @@ -161,6 +162,7 @@ private function initEnv(Script $script): Environment $twig->addExtension(new PhpSyntaxExtension()); $twig->addExtension($this->translationExtension); + $twig->addExtension(new SecurityExtension([])); if ($script->getTwigOptions()['debug'] ?? false) { $twig->addExtension(new DebugExtension());
tests/unit/php/Core/Framework/Adapter/Twig/SecurityExtensionTest.php+120 −0 added@@ -0,0 +1,120 @@ +<?php declare(strict_types=1); + +namespace Shopware\Tests\Unit\Core\Framework\Adapter\Twig; + +use PHPUnit\Framework\TestCase; +use Shopware\Core\Framework\Adapter\Twig\SecurityExtension; +use Twig\Environment; +use Twig\Error\RuntimeError; +use Twig\Loader\ArrayLoader; + +/** + * @internal + * @covers \Shopware\Core\Framework\Adapter\Twig\SecurityExtension + */ +class SecurityExtensionTest extends TestCase +{ + public function testMapNotAllowedFunction(): void + { + $this->expectException(RuntimeError::class); + $this->runTwig('{{ ["a", "b", "c"]|map("str_rot13")|join }}'); + } + + public function testMapWithAllowedFunction(): void + { + static::assertSame('nop', $this->runTwig('{{ ["a", "b", "c"]|map("str_rot13")|join }}', ['str_rot13'])); + } + + public function testMapWithClosure(): void + { + static::assertSame('a-testb-testc-test', $this->runTwig('{{ ["a", "b", "c"]|map(v => (v ~ "-test"))|join }}')); + } + + public function testReduceNotAllowedFunction(): void + { + $this->expectException(RuntimeError::class); + $this->runTwig('{{ ["a", "b", "c"]|reduce("empty")|join }}'); + } + + public function testReduceAllowedFunction(): void + { + static::assertSame('6', $this->runTwig('{{ [1 , 5]|reduce((a, b) => a + b)|json_encode|raw }}')); + } + + public function testReduceOnIterator(): void + { + static::assertSame('3', $this->runTwig('{{ test|reduce((a, b) => a + b)|json_encode|raw }}', [], ['test' => new \ArrayIterator([1, 2])])); + } + + public function testFilterNotAllowedFunctionWithAllowedFunction(): void + { + $this->expectException(RuntimeError::class); + $this->runTwig('{{ ["a", "b", "c"]|filter("str_rot13")|join }}'); + } + + public function testFilterClosure(): void + { + static::assertSame('a', $this->runTwig('{{ ["a", "b", "c"]|filter(v => v == "a")|join }}')); + } + + public function testFilterIteratorClosure(): void + { + static::assertSame( + 'a', + $this->runTwig('{{ test|filter(v => v == "a")|join }}', [], ['test' => new \ArrayIterator(['a', 'b', 'c'])]) + ); + } + + public function testSortNotAllowedFunction(): void + { + $this->expectException(RuntimeError::class); + $this->runTwig('{{ ["a", "b", "c"]|sort("str_rot13")|join }}'); + } + + public function testSortAllowedFunction(): void + { + set_error_handler(static function () { + return true; + }); + + static::assertSame('abc', $this->runTwig('{{ ["a", "b", "c"]|sort("str_starts_with")|join }}', ['str_starts_with'])); + + restore_error_handler(); + } + + public function testSortClosure(): void + { + static::assertSame('cba', $this->runTwig('{{ ["a", "b", "c"]|sort((a, b) => b <=> a)|join }}')); + } + + public function testSortIteratorClosure(): void + { + static::assertSame( + 'cba', + $this->runTwig('{{ test|sort((a, b) => b <=> a)|join }}', [], ['test' => new \ArrayIterator(['a', 'b', 'c'])]) + ); + } + + public function testSortDefault(): void + { + static::assertSame( + '123', + $this->runTwig('{{ test|sort|join }}', [], ['test' => ['2', '3', '1']]) + ); + } + + /** + * @param array<string> $allowedFunctions + * @param array<mixed> $variables + */ + private function runTwig(string $template, array $allowedFunctions = [], array $variables = []): string + { + $twig = new Environment(new ArrayLoader([ + 'test' => $template, + ])); + + $twig->addExtension(new SecurityExtension($allowedFunctions)); + + return $twig->render('test', $variables); + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-93cw-f5jj-x85wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-22731ghsaADVISORY
- docs.shopware.com/en/shopware-6-en/security-updates/security-update-01-2023ghsax_refsource_MISCWEB
- github.com/shopware/platform/commit/89d1ea154689cb6202e0d3a0ceeae0febb0c09e1ghsax_refsource_MISCWEB
- github.com/shopware/platform/security/advisories/GHSA-93cw-f5jj-x85wghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.