Skip to content

Commit 4ab0179

Browse files
authored
Merge pull request #18208 from craftcms/feature/disallow-string-arrows
Disallow non-Closure arrow functions
2 parents 4a86923 + a846cc8 commit 4ab0179

File tree

4 files changed

+84
-38
lines changed

4 files changed

+84
-38
lines changed

src/config/GeneralConfig.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,24 @@ class GeneralConfig extends BaseConfig
12241224
*/
12251225
public bool $enableTemplateCaching = true;
12261226

1227+
/**
1228+
* @var bool Whether all Twig templates should be sandboxed.
1229+
*
1230+
* ::: code
1231+
* ```php Static Config
1232+
* ->enableTwigSandbox(false)
1233+
* ```
1234+
* ```shell Environment Override
1235+
* CRAFT_ENABLE_TWIG_SANDBOX=false
1236+
* ```
1237+
* :::
1238+
*
1239+
* @see enableTwigSandbox()
1240+
* @group Security
1241+
* @since 4.17.0
1242+
*/
1243+
public bool $enableTwigSandbox = false;
1244+
12271245
/**
12281246
* @var string The prefix that should be prepended to HTTP error status codes when determining the path to look for an error’s template.
12291247
*
@@ -4564,6 +4582,25 @@ public function enableTemplateCaching(bool $value = true): self
45644582
return $this;
45654583
}
45664584

4585+
/**
4586+
* Whether all Twig templates should be sandboxed.
4587+
*
4588+
* ```php
4589+
* ->enableTwigSandbox(false)
4590+
* ```
4591+
*
4592+
* @group Security
4593+
* @param bool $value
4594+
* @return self
4595+
* @see $enableTwigSandbox
4596+
* @since 4.17.0
4597+
*/
4598+
public function enableTwigSandbox(bool $value = true): self
4599+
{
4600+
$this->enableTwigSandbox = $value;
4601+
return $this;
4602+
}
4603+
45674604
/**
45684605
* The prefix that should be prepended to HTTP error status codes when determining the path to look for an error’s template.
45694606
*

src/web/View.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use craft\web\twig\Extension;
2626
use craft\web\twig\FeExtension;
2727
use craft\web\twig\GlobalsExtension;
28+
use craft\web\twig\SecurityPolicy;
2829
use craft\web\twig\SinglePreloaderExtension;
2930
use craft\web\twig\TemplateLoader;
3031
use Illuminate\Support\Collection;
@@ -35,6 +36,7 @@
3536
use Twig\Error\SyntaxError as TwigSyntaxError;
3637
use Twig\Extension\CoreExtension;
3738
use Twig\Extension\ExtensionInterface;
39+
use Twig\Extension\SandboxExtension;
3840
use Twig\Extension\StringLoaderExtension;
3941
use Twig\Template as TwigTemplate;
4042
use Twig\TemplateWrapper;
@@ -395,6 +397,9 @@ public function createTwig(): Environment
395397

396398
$twig = new Environment(new TemplateLoader($this), $this->_getTwigOptions());
397399

400+
// Even an empty security policy will prevent non-closures from being allowed as arrow functions
401+
$twig->addExtension(new SandboxExtension(new SecurityPolicy(), Craft::$app->getConfig()->getGeneral()->enableTwigSandbox));
402+
398403
$twig->addExtension(new StringLoaderExtension());
399404
$twig->addExtension(new Extension($this, $twig));
400405

src/web/twig/Extension.php

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class Extension extends AbstractExtension implements GlobalsInterface
9999
*/
100100
public static function arraySome(TwigEnvironment $env, $array, $arrow)
101101
{
102-
self::checkArrowFunction($arrow, 'has some', 'operator');
102+
CoreExtension::checkArrow($env, $arrow, 'has some', 'operator');
103103
return CoreExtension::arraySome($env, $array, $arrow);
104104
}
105105

@@ -108,38 +108,10 @@ public static function arraySome(TwigEnvironment $env, $array, $arrow)
108108
*/
109109
public static function arrayEvery(TwigEnvironment $env, $array, $arrow)
110110
{
111-
self::checkArrowFunction($arrow, 'has every', 'operator');
111+
CoreExtension::checkArrow($env, $arrow, 'has every', 'operator');
112112
return CoreExtension::arrayEvery($env, $array, $arrow);
113113
}
114114

115-
/**
116-
* Called by:
117-
* - has every (operator)
118-
* - has some (operator)
119-
* - |filter
120-
* - |find
121-
* - |map
122-
* - |reduce
123-
* - |sort
124-
*/
125-
private static function checkArrowFunction(mixed $arrow, string $thing, string $type): void
126-
{
127-
if (
128-
is_string($arrow) &&
129-
in_array(ltrim(strtolower($arrow), '\\'), [
130-
'system',
131-
'passthru',
132-
'exec',
133-
'file_get_contents',
134-
'file_put_contents',
135-
'popen',
136-
'call_user_func',
137-
])
138-
) {
139-
throw new RuntimeError(sprintf('The "%s" %s does not support passing "%s".', $thing, $type, $arrow));
140-
}
141-
}
142-
143115
/**
144116
* @var View|null
145117
*/
@@ -253,7 +225,7 @@ public function getFilters(): array
253225
new TwigFilter('filesize', [$this, 'filesizeFilter']),
254226
new TwigFilter('filter', [$this, 'filterFilter'], ['needs_environment' => true]),
255227
new TwigFilter('filterByValue', [ArrayHelper::class, 'where'], ['deprecation_info' => new DeprecatedCallableInfo('craftcms/cms', '3.5.0', 'where')]),
256-
new TwigFilter('group', [$this, 'groupFilter']),
228+
new TwigFilter('group', [$this, 'groupFilter'], ['needs_environment' => true]),
257229
new TwigFilter('hash', [$security, 'hashData']),
258230
new TwigFilter('httpdate', [$this, 'httpdateFilter'], ['needs_environment' => true]),
259231
new TwigFilter('id', [Html::class, 'id']),
@@ -565,7 +537,7 @@ public function snakeFilter(mixed $string): string
565537
*/
566538
public function sortFilter(TwigEnvironment $env, iterable $array, string|callable|null $arrow = null): array
567539
{
568-
self::checkArrowFunction($arrow, 'sort', 'filter');
540+
CoreExtension::checkArrow($env, $arrow, 'sort', 'filter');
569541
return CoreExtension::sort($env, $array, $arrow);
570542
}
571543

@@ -582,7 +554,7 @@ public function sortFilter(TwigEnvironment $env, iterable $array, string|callabl
582554
*/
583555
public function reduceFilter(TwigEnvironment $env, mixed $array, mixed $arrow, mixed $initial = null): mixed
584556
{
585-
self::checkArrowFunction($arrow, 'reduce', 'filter');
557+
CoreExtension::checkArrow($env, $arrow, 'reduce', 'filter');
586558
return CoreExtension::reduce($env, $array, $arrow, $initial);
587559
}
588560

@@ -598,7 +570,7 @@ public function reduceFilter(TwigEnvironment $env, mixed $array, mixed $arrow, m
598570
*/
599571
public function mapFilter(TwigEnvironment $env, mixed $array, mixed $arrow = null): array
600572
{
601-
self::checkArrowFunction($arrow, 'map', 'filter');
573+
CoreExtension::checkArrow($env, $arrow, 'map', 'filter');
602574
return CoreExtension::map($env, $array, $arrow);
603575
}
604576

@@ -723,7 +695,7 @@ public function timestampFilter(mixed $value, ?string $format = null, bool $with
723695
*/
724696
public function findFilter(TwigEnvironment $env, $array, $arrow): mixed
725697
{
726-
self::checkArrowFunction($arrow, 'find', 'filter');
698+
CoreExtension::checkArrow($env, $arrow, 'find', 'filter');
727699
return CoreExtension::find($env, $array, $arrow);
728700
}
729701

@@ -1202,7 +1174,7 @@ public function encencFilter(mixed $str): string
12021174
*/
12031175
public function filterFilter(TwigEnvironment $env, iterable $arr, ?callable $arrow = null): array
12041176
{
1205-
self::checkArrowFunction($arrow, 'filter', 'filter');
1177+
CoreExtension::checkArrow($env, $arrow, 'filter', 'filter');
12061178

12071179
/** @var array|Traversable $arr */
12081180
if ($arrow === null) {
@@ -1224,14 +1196,15 @@ public function filterFilter(TwigEnvironment $env, iterable $arr, ?callable $arr
12241196
/**
12251197
* Groups an array by the results of an arrow function, or value of a property.
12261198
*
1199+
* @param TwigEnvironment $env
12271200
* @param iterable $arr
12281201
* @param callable|string $arrow The arrow function or property name that determines the group the item should be grouped in
12291202
* @return array[] The grouped items
12301203
* @throws RuntimeError if $arr is not of type array or Traversable
12311204
*/
1232-
public function groupFilter(iterable $arr, callable|string $arrow): array
1205+
public function groupFilter(TwigEnvironment $env, iterable $arr, callable|string $arrow): array
12331206
{
1234-
self::checkArrowFunction($arrow, 'group', 'filter');
1207+
CoreExtension::checkArrow($env, $arrow, 'group', 'filter');
12351208

12361209
$groups = [];
12371210

src/web/twig/SecurityPolicy.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
/**
3+
* @link https://craftcms.com/
4+
* @copyright Copyright (c) Pixel & Tonic, Inc.
5+
* @license https://craftcms.github.io/license/
6+
*/
7+
8+
namespace craft\web\twig;
9+
10+
use Twig\Sandbox\SecurityPolicyInterface;
11+
12+
/**
13+
* Security policy
14+
*
15+
* @author Pixel & Tonic, Inc. <[email protected]>
16+
* @since 4.17.0
17+
*/
18+
class SecurityPolicy implements SecurityPolicyInterface
19+
{
20+
public function checkSecurity($tags, $filters, $functions): void
21+
{
22+
}
23+
24+
public function checkMethodAllowed($obj, $method): void
25+
{
26+
}
27+
28+
public function checkPropertyAllowed($obj, $property): void
29+
{
30+
}
31+
}

0 commit comments

Comments
 (0)