Skip to content

Commit

Permalink
NEXT-24667 - Backport Twig changes
Browse files Browse the repository at this point in the history
  • Loading branch information
shyim committed Jan 24, 2023
1 parent b568e59 commit 1c1ff84
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 4 deletions.
22 changes: 22 additions & 0 deletions changelog/_unreleased/2022-12-21-add-twig-filter-improvments.md
@@ -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" ]
```
16 changes: 16 additions & 0 deletions config-schema.json
Expand Up @@ -86,6 +86,9 @@
},
"profiler": {
"$ref": "#/definitions/profiler"
},
"twig": {
"$ref": "#/definitions/twig"
}
},
"title": "Shopware"
Expand All @@ -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,
Expand Down
5 changes: 1 addition & 4 deletions phpstan-baseline.neon
Expand Up @@ -4960,10 +4960,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: "#^Cannot call method map\\(\\) on Shopware\\\\Core\\\\System\\\\SalesChannel\\\\Aggregate\\\\SalesChannelDomain\\\\SalesChannelDomainCollection\\|null\\.$#"
Expand Down
6 changes: 6 additions & 0 deletions src/Core/Content/Seo/SeoUrlTwigFactory.php
Expand Up @@ -5,23 +5,29 @@
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;

/**
* @package sales-channel
*/
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);
Expand Down
114 changes: 114 additions & 0 deletions src/Core/Framework/Adapter/Twig/SecurityExtension.php
@@ -0,0 +1,114 @@
<?php declare(strict_types=1);

namespace Shopware\Core\Framework\Adapter\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;

/**
* @internal
*/
class SecurityExtension extends AbstractExtension
{
/**
* @param array<string> $allowedPHPFunctions
*/
public function __construct(private readonly array $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
*
* @return array<mixed>
*/
public function map(iterable $array, string|callable|\Closure $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
*/
public function reduce(iterable $array, string|callable|\Closure $function, mixed $initial = null): mixed
{
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
*
* @return iterable<mixed>
*/
public function filter(iterable $array, string|callable|\Closure $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
*
* @return array<mixed>
*/
public function sort(iterable $array, string|callable|\Closure|null $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;
}
}
17 changes: 17 additions & 0 deletions src/Core/Framework/DependencyInjection/Configuration.php
Expand Up @@ -39,6 +39,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->append($this->createCacheSection())
->append($this->createHtmlSanitizerSection())
->append($this->createIncrementSection())
->append($this->createTwigSection())
->end();

return $treeBuilder;
Expand Down Expand Up @@ -607,4 +608,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;
}
}
5 changes: 5 additions & 0 deletions src/Core/Framework/DependencyInjection/services.xml
Expand Up @@ -387,6 +387,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>
Expand Down
3 changes: 3 additions & 0 deletions src/Core/Framework/Resources/config/packages/shopware.yaml
Expand Up @@ -255,3 +255,6 @@ shopware:
country_state_route: []
salutation_route: []
sitemap_route: []

twig:
allowed_php_functions: []
3 changes: 3 additions & 0 deletions src/Core/Framework/Rule/ScriptRule.php
Expand Up @@ -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;
Expand Down Expand Up @@ -91,6 +92,8 @@ public function match(RuleScope $scope): bool
$twig->addExtension(new DebugExtension());
}

$twig->addExtension(new SecurityExtension([]));

$hook = new AppScriptConditionHook($scope->getContext());

try {
Expand Down
2 changes: 2 additions & 0 deletions src/Core/Framework/Script/Execution/ScriptExecutor.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit 1c1ff84

Please sign in to comment.