VYPR
Critical severityNVD Advisory· Published Jan 17, 2023· Updated Mar 10, 2025

Improper Control of Generation of Code in Twig rendered views in shopware

CVE-2023-22731

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.

PackageAffected versionsPatched versions
shopware/platformPackagist
< 6.4.18.16.4.18.1
shopware/corePackagist
< 6.4.18.16.4.18.1

Affected products

3

Patches

1
89d1ea154689

NEXT-24667 - Add twig improvements

https://github.com/shopware/platformSoner SayakciDec 21, 2022via ghsa
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

News mentions

0

No linked articles in our index yet.