CVE-2023-3393: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value:  Code Injection in GitHub repository fossbilling/fossbilling prior to 0.5.1.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 7.2
Published: 2023-06-23T19:15Z
Repository: https://github.com/fossbilling/fossbilling
Language: PHP
Files changed: src/library/Box/TwigExtensions.php
Commit date: 2023-06-20 05:31:20 -0700
Commit message: Prevent twig from calling unsafe functions (#1337)

* Prevent twig from calling unsafe functions

* Just prevent anything that's not a closure

* Formatting
Diff: diff --git a/src/library/Box/TwigExtensions.php b/src/library/Box/TwigExtensions.php
index 3c4ddd7ef..3c6ecfc7c 100644
--- a/src/library/Box/TwigExtensions.php
+++ b/src/library/Box/TwigExtensions.php
@@ -33,61 +33,50 @@ class Box_TwigExtensions extends AbstractExtension implements InjectionAwareInte
      * @return array An array of filters
      */
     public function getFilters()
     {
         return [
-            /**
-             * 'trans' filter rewrite same filter from Symfony\Bridge\Twig\Extension\TranslationExtension
-             * for compatible with outdated php-gettext library.
-             *
-             * TODO: Use Symfony\Component\Translation\Loader\MoFileLoader and remove php-gettext hardcoded library.
-             */
             'trans' => new TwigFilter('trans', '__trans'),
 
             'alink' => new TwigFilter('alink', [$this, 'twig_bb_admin_link_filter'], ['is_safe' => ['html']]),
-
             'link' => new TwigFilter('link', [$this, 'twig_bb_client_link_filter'], ['is_safe' => ['html']]),
+            'autolink' => new TwigFilter('autolink', [$this, 'twig_autolink_filter']),
 
             'gravatar' => new TwigFilter('gravatar', [$this, 'twig_gravatar_filter']),
 
             'markdown' => new TwigFilter('markdown', [$this, 'twig_markdown_filter'], ['needs_environment' => true, 'is_safe' => ['html']]),
 
             'truncate' => new TwigFilter('truncate', [$this, 'twig_truncate_filter'], ['needs_environment' => true]),
 
             'timeago' => new TwigFilter('timeago', [$this, 'twig_timeago_filter']),
-
             'daysleft' => new TwigFilter('daysleft', [$this, 'twig_daysleft_filter']),
 
             'size' => new TwigFilter('size', [$this, 'twig_size_filter']),
 
             'ipcountryname' => new TwigFilter('ipcountryname', [$this, 'twig_ipcountryname_filter']),
 
             'number' => new TwigFilter('number', [$this, 'twig_number_filter']),
 
             'period_title' => new TwigFilter('period_title', [$this, 'twig_period_title'], ['needs_environment' => true, 'is_safe' => ['html']]),
 
-            'autolink' => new TwigFilter('autolink', [$this, 'twig_autolink_filter']),
-
             'img_tag' => new TwigFilter('img_tag', [$this, 'twig_img_tag'], ['needs_environment' => false, 'is_safe' => ['html']]),
-
             'script_tag' => new TwigFilter('script_tag', [$this, 'twig_script_tag'], ['needs_environment' => false, 'is_safe' => ['html']]),
-
             'stylesheet_tag' => new TwigFilter('stylesheet_tag', [$this, 'twig_stylesheet_tag'], ['needs_environment' => false, 'is_safe' => ['html']]),
-
             'mod_asset_url' => new TwigFilter('mod_asset_url', [$this, 'twig_mod_asset_url']),
-
             'asset_url' => new TwigFilter('asset_url', [$this, 'twig_asset_url'], ['needs_environment' => true, 'is_safe' => ['html']]),
-
             'library_url' => new TwigFilter('library_url', [$this, 'twig_library_url'], ['is_safe' => ['html']]),
 
             'money' => new TwigFilter('money', [$this, 'twig_money'], ['needs_environment' => true, 'is_safe' => ['html']]),
-
             'money_without_currency' => new TwigFilter('money_without_currency', [$this, 'twig_money_without_currency'], ['needs_environment' => true, 'is_safe' => ['html']]),
-
             'money_convert' => new TwigFilter('money_convert', [$this, 'twig_money_convert'], ['needs_environment' => true, 'is_safe' => ['html']]),
-
             'money_convert_without_currency' => new TwigFilter('money_convert_without_currency', [$this, 'money_convert_without_currency'], ['needs_environment' => true, 'is_safe' => ['html']]),
+
+            // We override these default twig filters so we can explicitly disable it from calling certain functions that may leak data or allow commands to be executed on the system.
+            'filter' => new TwigFilter('filter', [$this, 'filteredFilter']),
+            'map' => new TwigFilter('map', [$this, 'filteredMap']),
+            'reduce' => new TwigFilter('reduce', [$this, 'filteredReduce']),
+            'sort' => new TwigFilter('sort', [$this, 'filteredSort']),
         ];
     }
 
     /**
      * Returns the name of the extension.
@@ -316,6 +305,64 @@ class Box_TwigExtensions extends AbstractExtension implements InjectionAwareInte
             return mb_substr($value, 0, $length) . $separator;
         }
 
         return $value;
     }
+
+    public function filteredFilter($array, $arrow)
+    {
+        if (!$arrow instanceof Closure) {
+            return;
+        }
+
+        return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
+    }
+
+    public function filteredMap($array, $arrow)
+    {
+        if (!$arrow instanceof Closure) {
+            return;
+        }
+
+        $r = [];
+        foreach ($array as $k => $v) {
+            $r[$k] = $arrow($v, $k);
+        }
+
+        return $r;
+    }
+
+    public function filteredReduce($array, $arrow, $initial = null)
+    {
+        if (!$arrow instanceof Closure) {
+            return;
+        }
+
+        $accumulator = $initial;
+        foreach ($array as $key => $value) {
+            $accumulator = $arrow($accumulator, $value, $key);
+        }
+
+        return $accumulator;
+    }
+
+    public function filteredSort($array, $arrow)
+    {
+        if (!$arrow instanceof Closure) {
+            return;
+        }
+
+        if ($array instanceof \Traversable) {
+            $array = iterator_to_array($array);
+        } elseif (!\is_array($array)) {
+            return $array;
+        }
+
+        if (null !== $arrow) {
+            uasort($array, $arrow);
+        } else {
+            asort($array);
+        }
+
+        return $array;
+    }
 }

Vulnerable code:             /**
             * 'trans' filter rewrite same filter from Symfony\Bridge\Twig\Extension\TranslationExtension
             * for compatible with outdated php-gettext library.
             *
             * TODO: Use Symfony\Component\Translation\Loader\MoFileLoader and remove php-gettext hardcoded library.
             */


            'autolink' => new TwigFilter('autolink', [$this, 'twig_autolink_filter']),









Fixed code:             'autolink' => new TwigFilter('autolink', [$this, 'twig_autolink_filter']),

            // We override these default twig filters so we can explicitly disable it from calling certain functions that may leak data or allow commands to be executed on the system.
            'filter' => new TwigFilter('filter', [$this, 'filteredFilter']),
            'map' => new TwigFilter('map', [$this, 'filteredMap']),
            'reduce' => new TwigFilter('reduce', [$this, 'filteredReduce']),
            'sort' => new TwigFilter('sort', [$this, 'filteredSort']),

    public function filteredFilter($array, $arrow)
    {
        if (!$arrow instanceof Closure) {
            return;
        }

        return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);
    }

    public function filteredMap($array, $arrow)
    {
        if (!$arrow instanceof Closure) {
            return;
        }

        $r = [];
        foreach ($array as $k => $v) {
            $r[$k] = $arrow($v, $k);
        }

        return $r;
    }

    public function filteredReduce($array, $arrow, $initial = null)
    {
        if (!$arrow instanceof Closure) {
            return;
        }

        $accumulator = $initial;
        foreach ($array as $key => $value) {
            $accumulator = $arrow($accumulator, $value, $key);
        }

        return $accumulator;
    }

    public function filteredSort($array, $arrow)
    {
        if (!$arrow instanceof Closure) {
            return;
        }

        if ($array instanceof \Traversable) {
            $array = iterator_to_array($array);
        } elseif (!\is_array($array)) {
            return $array;
        }

        if (null !== $arrow) {
            uasort($array, $arrow);
        } else {
            asort($array);
        }

        return $array;
    }