CVE-2021-29454: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic. Prior to versions 3.1.42 and 4.0.2, template authors could run arbitrary PHP code by crafting a malicious math string. If a math string was passed through as user provided data to the math function, external users could run arbitrary PHP code by crafting a malicious math string. Users should upgrade to version 3.1.42 or 4.0.2 to receive a patch.
CWE description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
CVSS v3 base score: 8.8
Published: 2022-01-10T20:15Z
Repository: https://github.com/smarty-php/smarty
Language: PHP
Files changed: CHANGELOG.md, libs/plugins/function.math.php, tests/UnitTests/TemplateSource/ValueTests/Math/MathTest.php
Commit date: 2022-01-10 00:01:43 +0100
Commit message: Merge pull request from GHSA-29gp-2c3m-3j6m

* Temporary fix. Waiting for CVE

* Add CVE
Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c302b24d..f35e0be32 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,13 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
 
+### Security
+- Prevent arbitrary PHP code execution through maliciously crafted expression for the math function. This addresses CVE-2021-29454
+
 ## [4.0.1] - 2022-01-09
 
 ### Security
 - Rewrote the mailto function to not use `eval` when encoding with javascript
 
diff --git a/libs/plugins/function.math.php b/libs/plugins/function.math.php
index 5d58284fc..442b04c78 100644
--- a/libs/plugins/function.math.php
+++ b/libs/plugins/function.math.php
@@ -26,42 +26,71 @@ function smarty_function_math($params, $template)
     static $_allowed_funcs =
         array(
             'int'   => true,
             'abs'   => true,
             'ceil'  => true,
+            'acos'   => true,
+            'acosh'   => true,
             'cos'   => true,
+            'cosh'   => true,
+            'deg2rad'   => true,
+            'rad2deg'   => true,
             'exp'   => true,
             'floor' => true,
             'log'   => true,
             'log10' => true,
             'max'   => true,
             'min'   => true,
             'pi'    => true,
             'pow'   => true,
             'rand'  => true,
             'round' => true,
+            'asin'   => true,
+            'asinh'   => true,
             'sin'   => true,
+            'sinh'   => true,
             'sqrt'  => true,
             'srand' => true,
-            'tan'   => true
+            'atan'   => true,
+            'atanh'   => true,
+            'tan'   => true,
+            'tanh'   => true
         );
+
     // be sure equation parameter is present
     if (empty($params[ 'equation' ])) {
         trigger_error("math: missing equation parameter", E_USER_WARNING);
         return;
     }
     $equation = $params[ 'equation' ];
+
+    // Remove whitespaces
+    $equation = preg_replace('/\s+/', '', $equation);
+
+    // Adapted from https://www.php.net/manual/en/function.eval.php#107377
+    $number = '(?:\d+(?:[,.]\d+)?|pi|π)'; // What is a number
+    $functionsOrVars = '((?:0x[a-fA-F0-9]+)|([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*))';
+    $operators = '[+\/*\^%-]'; // Allowed math operators
+    $regexp = '/^(('.$number.'|'.$functionsOrVars.'|('.$functionsOrVars.'\s*\((?1)+\)|\((?1)+\)))(?:'.$operators.'(?2))?)+$/';
+
+    if (!preg_match($regexp, $equation)) {
+        trigger_error("math: illegal characters", E_USER_WARNING);
+        return;
+    }
+
     // make sure parenthesis are balanced
     if (substr_count($equation, '(') !== substr_count($equation, ')')) {
         trigger_error("math: unbalanced parenthesis", E_USER_WARNING);
         return;
     }
+
     // disallow backticks
     if (strpos($equation, '`') !== false) {
         trigger_error("math: backtick character not allowed in equation", E_USER_WARNING);
         return;
     }
+
     // also disallow dollar signs
     if (strpos($equation, '$') !== false) {
         trigger_error("math: dollar signs not allowed in equation", E_USER_WARNING);
         return;
     }
@@ -94,10 +123,11 @@ function smarty_function_math($params, $template)
             $equation = preg_replace("/\b$key\b/", " \$params['$key'] ", $equation);
         }
     }
     $smarty_math_result = null;
     eval("\$smarty_math_result = " . $equation . ";");
+
     if (empty($params[ 'format' ])) {
         if (empty($params[ 'assign' ])) {
             return $smarty_math_result;
         } else {
             $template->assign($params[ 'assign' ], $smarty_math_result);
diff --git a/tests/UnitTests/TemplateSource/ValueTests/Math/MathTest.php b/tests/UnitTests/TemplateSource/ValueTests/Math/MathTest.php
index 9c45d36e6..7506f1670 100644
--- a/tests/UnitTests/TemplateSource/ValueTests/Math/MathTest.php
+++ b/tests/UnitTests/TemplateSource/ValueTests/Math/MathTest.php
@@ -105,6 +105,37 @@ class MathTest extends PHPUnit_Smarty
         $this->smarty->disableSecurity();
         $expected = "22.00 -- 4.10";
         $tpl = $this->smarty->createTemplate('eval:{$x = "4"}{$y = "5.5"}{math equation="x * y" x=$x y=$y format="%0.2f"} -- {math equation="20.5 / 5" format="%0.2f"}');
         $this->assertEquals($expected, $this->smarty->fetch($tpl));
     }
+
+	/**
+	 * @expectedException PHPUnit_Framework_Error_Warning
+	 */
+	public function testBackticksIllegal()
+	{
+		$expected = "22.00";
+		$tpl = $this->smarty->createTemplate('eval:{$x = "4"}{$y = "5.5"}{math equation="`ls` x * y" x=$x y=$y}');
+		$this->assertEquals($expected, $this->smarty->fetch($tpl));
+	}
+
+	/**
+	 * @expectedException PHPUnit_Framework_Error_Warning
+	 */
+	public function testDollarSignsIllegal()
+	{
+		$expected = "22.00";
+		$tpl = $this->smarty->createTemplate('eval:{$x = "4"}{$y = "5.5"}{math equation="$" x=$x y=$y}');
+		$this->assertEquals($expected, $this->smarty->fetch($tpl));
+	}
+
+	/**
+	 * @expectedException PHPUnit_Framework_Error_Warning
+	 */
+	public function testBracketsIllegal()
+	{
+		$expected = "I";
+		$tpl = $this->smarty->createTemplate('eval:{$x = "0"}{$y = "1"}{math equation="((y/x).(x))[x]" x=$x y=$y}');
+		$this->assertEquals($expected, $this->smarty->fetch($tpl));
+	}
+
 }

Vulnerable code:             'tan'   => true
Fixed code: ### Security
- Prevent arbitrary PHP code execution through maliciously crafted expression for the math function. This addresses CVE-2021-29454

            'acos'   => true,
            'acosh'   => true,
            'cosh'   => true,
            'deg2rad'   => true,
            'rad2deg'   => true,
            'asin'   => true,
            'asinh'   => true,
            'sinh'   => true,
            'atan'   => true,
            'atanh'   => true,
            'tan'   => true,
            'tanh'   => true


    // Remove whitespaces
    $equation = preg_replace('/\s+/', '', $equation);

    // Adapted from https://www.php.net/manual/en/function.eval.php#107377
    $number = '(?:\d+(?:[,.]\d+)?|pi|π)'; // What is a number
    $functionsOrVars = '((?:0x[a-fA-F0-9]+)|([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*))';
    $operators = '[+\/*\^%-]'; // Allowed math operators
    $regexp = '/^(('.$number.'|'.$functionsOrVars.'|('.$functionsOrVars.'\s*\((?1)+\)|\((?1)+\)))(?:'.$operators.'(?2))?)+$/';

    if (!preg_match($regexp, $equation)) {
        trigger_error("math: illegal characters", E_USER_WARNING);
        return;
    }





	/**
	 * @expectedException PHPUnit_Framework_Error_Warning
	 */
	public function testBackticksIllegal()
	{
		$expected = "22.00";
		$tpl = $this->smarty->createTemplate('eval:{$x = "4"}{$y = "5.5"}{math equation="`ls` x * y" x=$x y=$y}');
		$this->assertEquals($expected, $this->smarty->fetch($tpl));
	}

	/**
	 * @expectedException PHPUnit_Framework_Error_Warning
	 */
	public function testDollarSignsIllegal()
	{
		$expected = "22.00";
		$tpl = $this->smarty->createTemplate('eval:{$x = "4"}{$y = "5.5"}{math equation="$" x=$x y=$y}');
		$this->assertEquals($expected, $this->smarty->fetch($tpl));
	}

	/**
	 * @expectedException PHPUnit_Framework_Error_Warning
	 */
	public function testBracketsIllegal()
	{
		$expected = "I";
		$tpl = $this->smarty->createTemplate('eval:{$x = "0"}{$y = "1"}{math equation="((y/x).(x))[x]" x=$x y=$y}');
		$this->assertEquals($expected, $this->smarty->fetch($tpl));
	}