VYPR
High severityNVD Advisory· Published Feb 4, 2022· Updated Apr 23, 2025

Code injection in Twig

CVE-2022-23614

Description

Twig's sandbox mode failed to enforce that the sort filter's arrow parameter is a Closure, allowing arbitrary PHP code injection.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Twig's sandbox mode failed to enforce that the `sort` filter's arrow parameter is a Closure, allowing arbitrary PHP code injection.

Vulnerability

Twig, an open-source PHP template language, provides a sandbox mode to restrict template authors from executing arbitrary PHP code. In affected versions (prior to the fix), the sort filter's arrow parameter was not properly validated to be a Closure when sandbox mode was enabled. This allowed attackers to pass arbitrary callables, including PHP function names, leading to code injection. The vulnerability affects all Twig versions before the patch introduced in commit [2eb3308] and [22b9dc3] [1][2][3].

Exploitation

An attacker with the ability to supply or modify Twig templates (e.g., in a multi-tenant environment or CMS where templates are user-controllable) can craft a template using the sort filter with a non-Closure arrow, such as a string naming a PHP function like 'system'. When the template is rendered in sandbox mode, the unsanitized callable is executed, bypassing the sandbox restrictions. No special network position or authentication is required beyond template authoring privileges [1][2].

Impact

Successful exploitation allows an attacker to execute arbitrary PHP code on the server. This can lead to full compromise of the application, including data disclosure, modification, or deletion, and potentially server takeover. The impact is critical as it bypasses the sandbox's intended security boundary [1][4].

Mitigation

Users should upgrade to Twig versions that include the fix: the patch was released on 2022-02-04. The fix introduces a helper function twig_check_arrow_in_sandbox that validates the arrow is a Closure in sandbox mode, and modifies the sort filter to require the environment parameter. No workaround is available; upgrading is the only mitigation [2][3][4].

AI Insight generated on May 21, 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
twig/twigPackagist
>= 2.0.0, < 2.14.112.14.11
twig/twigPackagist
>= 3.0.0, < 3.3.83.3.8

Affected products

2

Patches

2
22b9dc3c03ee

bug #3641 Disallow non closures in `sort` filter when the sanbox mode is enabled (fabpot)

https://github.com/twigphp/twigFabien PotencierFeb 4, 2022via ghsa
3 files changed · +18 15
  • CHANGELOG+3 3 modified
    @@ -1,10 +1,10 @@
    -# 2.14.11 (2022-XX-XX)
    +# 2.14.11 (2022-02-04)
     
    -* n/a
    + * Fix a security issue when in a sandbox: the `sort` filter must require a Closure for the `arrow` parameter
     
     # 2.14.10 (2022-01-03)
     
    -* Allow more null arguments when Twig expects a string (for better 8.1 support)
    + * Allow more null arguments when Twig expects a string (for better 8.1 support)
     
     # 2.14.9 (2022-01-03)
     
    
  • src/Extension/CoreExtension.php+14 11 modified
    @@ -237,7 +237,7 @@ public function getFilters()
                 // array helpers
                 new TwigFilter('join', 'twig_join_filter'),
                 new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]),
    -            new TwigFilter('sort', 'twig_sort_filter'),
    +            new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]),
                 new TwigFilter('merge', 'twig_array_merge'),
                 new TwigFilter('batch', 'twig_array_batch'),
                 new TwigFilter('column', 'twig_array_column'),
    @@ -926,7 +926,7 @@ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
      *
      * @return array
      */
    -function twig_sort_filter($array, $arrow = null)
    +function twig_sort_filter(Environment $env, $array, $arrow = null)
     {
         if ($array instanceof \Traversable) {
             $array = iterator_to_array($array);
    @@ -935,6 +935,8 @@ function twig_sort_filter($array, $arrow = null)
         }
     
         if (null !== $arrow) {
    +        twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter');
    +
             uasort($array, $arrow);
         } else {
             asort($array);
    @@ -1606,9 +1608,7 @@ function twig_array_filter(Environment $env, $array, $arrow)
             throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
         }
     
    -    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
    -        throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.');
    -    }
    +    twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter');
     
         if (\is_array($array)) {
             return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
    @@ -1620,9 +1620,7 @@ function twig_array_filter(Environment $env, $array, $arrow)
     
     function twig_array_map(Environment $env, $array, $arrow)
     {
    -    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
    -        throw new RuntimeError('The callable passed to the "map" filter must be a Closure in sandbox mode.');
    -    }
    +    twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter');
     
         $r = [];
         foreach ($array as $k => $v) {
    @@ -1634,9 +1632,7 @@ function twig_array_map(Environment $env, $array, $arrow)
     
     function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
     {
    -    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
    -        throw new RuntimeError('The callable passed to the "reduce" filter must be a Closure in sandbox mode.');
    -    }
    +    twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter');
     
         if (!\is_array($array)) {
             if (!$array instanceof \Traversable) {
    @@ -1648,4 +1644,11 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
     
         return array_reduce($array, $arrow, $initial);
     }
    +
    +function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
    +{
    +    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
    +        throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
    +    }
    +}
     }
    
  • tests/Extension/SandboxTest.php+1 1 modified
    @@ -390,7 +390,7 @@ public function testSandboxDisabledAfterIncludeFunctionError()
         public function testSandboxWithNoClosureFilter()
         {
             $this->expectException('\Twig\Error\RuntimeError');
    -        $this->expectExceptionMessage('The callable passed to "filter" filter must be a Closure in sandbox mode in "index" at line 1.');
    +        $this->expectExceptionMessage('The callable passed to the "filter" filter must be a Closure in sandbox mode in "index" at line 1.');
     
             $twig = $this->getEnvironment(true, ['autoescape' => 'html'], ['index' => <<<EOF
     {{ ["foo", "bar", ""]|filter("trim")|join(", ") }}
    
2eb330805586

Disallow non closures in `sort` filter when the sanbox mode is enabled

https://github.com/twigphp/twigFabien PotencierFeb 4, 2022via ghsa
3 files changed · +18 15
  • CHANGELOG+3 3 modified
    @@ -1,10 +1,10 @@
    -# 2.14.11 (2022-XX-XX)
    +# 2.14.11 (2022-02-04)
     
    -* n/a
    + * Fix a security issue when in a sandbox: the `sort` filter must require a Closure for the `arrow` parameter
     
     # 2.14.10 (2022-01-03)
     
    -* Allow more null arguments when Twig expects a string (for better 8.1 support)
    + * Allow more null arguments when Twig expects a string (for better 8.1 support)
     
     # 2.14.9 (2022-01-03)
     
    
  • src/Extension/CoreExtension.php+14 11 modified
    @@ -237,7 +237,7 @@ public function getFilters()
                 // array helpers
                 new TwigFilter('join', 'twig_join_filter'),
                 new TwigFilter('split', 'twig_split_filter', ['needs_environment' => true]),
    -            new TwigFilter('sort', 'twig_sort_filter'),
    +            new TwigFilter('sort', 'twig_sort_filter', ['needs_environment' => true]),
                 new TwigFilter('merge', 'twig_array_merge'),
                 new TwigFilter('batch', 'twig_array_batch'),
                 new TwigFilter('column', 'twig_array_column'),
    @@ -926,7 +926,7 @@ function twig_reverse_filter(Environment $env, $item, $preserveKeys = false)
      *
      * @return array
      */
    -function twig_sort_filter($array, $arrow = null)
    +function twig_sort_filter(Environment $env, $array, $arrow = null)
     {
         if ($array instanceof \Traversable) {
             $array = iterator_to_array($array);
    @@ -935,6 +935,8 @@ function twig_sort_filter($array, $arrow = null)
         }
     
         if (null !== $arrow) {
    +        twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter');
    +
             uasort($array, $arrow);
         } else {
             asort($array);
    @@ -1606,9 +1608,7 @@ function twig_array_filter(Environment $env, $array, $arrow)
             throw new RuntimeError(sprintf('The "filter" filter expects an array or "Traversable", got "%s".', \is_object($array) ? \get_class($array) : \gettype($array)));
         }
     
    -    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
    -        throw new RuntimeError('The callable passed to "filter" filter must be a Closure in sandbox mode.');
    -    }
    +    twig_check_arrow_in_sandbox($env, $arrow, 'filter', 'filter');
     
         if (\is_array($array)) {
             return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
    @@ -1620,9 +1620,7 @@ function twig_array_filter(Environment $env, $array, $arrow)
     
     function twig_array_map(Environment $env, $array, $arrow)
     {
    -    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
    -        throw new RuntimeError('The callable passed to the "map" filter must be a Closure in sandbox mode.');
    -    }
    +    twig_check_arrow_in_sandbox($env, $arrow, 'map', 'filter');
     
         $r = [];
         foreach ($array as $k => $v) {
    @@ -1634,9 +1632,7 @@ function twig_array_map(Environment $env, $array, $arrow)
     
     function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
     {
    -    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
    -        throw new RuntimeError('The callable passed to the "reduce" filter must be a Closure in sandbox mode.');
    -    }
    +    twig_check_arrow_in_sandbox($env, $arrow, 'reduce', 'filter');
     
         if (!\is_array($array)) {
             if (!$array instanceof \Traversable) {
    @@ -1648,4 +1644,11 @@ function twig_array_reduce(Environment $env, $array, $arrow, $initial = null)
     
         return array_reduce($array, $arrow, $initial);
     }
    +
    +function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type)
    +{
    +    if (!$arrow instanceof Closure && $env->hasExtension('\Twig\Extension\SandboxExtension') && $env->getExtension('\Twig\Extension\SandboxExtension')->isSandboxed()) {
    +        throw new RuntimeError(sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type));
    +    }
    +}
     }
    
  • tests/Extension/SandboxTest.php+1 1 modified
    @@ -390,7 +390,7 @@ public function testSandboxDisabledAfterIncludeFunctionError()
         public function testSandboxWithNoClosureFilter()
         {
             $this->expectException('\Twig\Error\RuntimeError');
    -        $this->expectExceptionMessage('The callable passed to "filter" filter must be a Closure in sandbox mode in "index" at line 1.');
    +        $this->expectExceptionMessage('The callable passed to the "filter" filter must be a Closure in sandbox mode in "index" at line 1.');
     
             $twig = $this->getEnvironment(true, ['autoescape' => 'html'], ['index' => <<<EOF
     {{ ["foo", "bar", ""]|filter("trim")|join(", ") }}
    

Vulnerability mechanics

Root cause

"Missing sandbox enforcement in the `sort` filter's `arrow` parameter allowed non-Closure callables to be executed via `uasort()`, enabling arbitrary PHP code injection."

Attack vector

An attacker who can control a Twig template in sandbox mode passes a non-Closure callable (e.g. a PHP function name as a string) to the `arrow` parameter of the `sort` filter. Because the sandbox did not verify that the `arrow` argument is a Closure, the callable is passed directly to `uasort()` and executed, allowing arbitrary PHP function calls [CWE-94]. The precondition is that the Twig environment has the Sandbox extension enabled and the attacker can supply template content (e.g. through a template injection or a template-editing feature).

Affected code

The vulnerability is in the `twig_sort_filter` function in `src/Extension/CoreExtension.php`. Before the patch, the `sort` filter did not pass the `Environment` object (`needs_environment` was false) and the function signature was `twig_sort_filter($array, $arrow = null)` with no sandbox check on the `$arrow` parameter [patch_id=1699770][patch_id=1699771]. Other filters (`filter`, `map`, `reduce`) already had inline sandbox checks, but `sort` was missing this enforcement.

What the fix does

The patch adds `'needs_environment' => true` to the `sort` filter registration and changes `twig_sort_filter` to accept `Environment $env` as its first parameter [patch_id=1699770][patch_id=1699771]. Inside the function, a call to the new helper `twig_check_arrow_in_sandbox($env, $arrow, 'sort', 'filter')` is inserted before `uasort()`. This helper throws a `RuntimeError` if the `$arrow` is not a `Closure` and the sandbox is active, closing the code injection vector. The same helper also replaces the duplicated inline checks in `filter`, `map`, and `reduce` filters, centralizing the enforcement.

Preconditions

  • configTwig Sandbox extension must be enabled and sandbox mode active
  • inputAttacker must be able to supply or control Twig template content (e.g. via template injection)

Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

16

News mentions

0

No linked articles in our index yet.