CVE-2024-35226: Insufficient Information (NVD-CWE-noinfo)
value: Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic. In affected versions template authors could inject php code by choosing a malicious file name for an extends-tag. Sites that cannot fully trust template authors should update asap. All users are advised to update. There is no patch for users on the v3 branch. There are no known workarounds for this vulnerability.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-28T21:16Z
Repository: https://github.com/smarty-php/smarty
Language: PHP
Files changed: changelog/GHSA-4rmg-292m-wg3w.md, src/Compile/Tag/ExtendsTag.php, src/Compiler/Template.php, src/Smarty.php, tests/UnitTests/TemplateSource/TagTests/BockExtend/CompileBlockExtendsTest.php, tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping.tpl, tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping2.tpl, tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping3.tpl, tests/UnitTests/TemplateSource/TagTests/Include/CompileIncludeTest.php, tests/UnitTests/TemplateSource/TagTests/Include/templates/test_include_security.tpl, tests/UnitTests/TemplateSource/_Issues/419/ExtendsIssue419Test.php
Commit date: 2024-05-28 22:44:30 +0200
Commit message: Merge pull request from GHSA-4rmg-292m-wg3w


Diff: diff --git a/changelog/GHSA-4rmg-292m-wg3w.md b/changelog/GHSA-4rmg-292m-wg3w.md
new file mode 100644
index 000000000..c0e28bc16
--- /dev/null
+++ b/changelog/GHSA-4rmg-292m-wg3w.md
@@ -0,0 +1,2 @@
+- Fixed a code injection vulnerability in extends-tag. This addresses CVE-2024-35226.
+- Added `$smarty->setCacheModifiedCheck()` setter for cache_modified_check
\ No newline at end of file
diff --git a/src/Compile/Tag/ExtendsTag.php b/src/Compile/Tag/ExtendsTag.php
index dcdbbc976..0ec2cf2ba 100644
--- a/src/Compile/Tag/ExtendsTag.php
+++ b/src/Compile/Tag/ExtendsTag.php
@@ -30,11 +30,11 @@ class ExtendsTag extends Inheritance {
 	 * Array of names of optional attribute required by tag
 	 * use array('_any') if there is no restriction of attributes names
 	 *
 	 * @var array
 	 */
-	protected $optional_attributes = ['extends_resource'];
+	protected $optional_attributes = [];
 
 	/**
 	 * Attribute definition: Overwrites base class.
 	 *
 	 * @var array
@@ -62,33 +62,11 @@ class ExtendsTag extends Inheritance {
 		if (strpos($_attr['file'], '$_tmp') !== false) {
 			$compiler->trigger_template_error('illegal value for file attribute', $compiler->getParser()->lex->line - 1);
 		}
 		// add code to initialize inheritance
 		$this->registerInit($compiler, true);
-		$file = trim($_attr['file'], '\'"');
-		if (strlen($file) > 8 && substr($file, 0, 8) === 'extends:') {
-			// generate code for each template
-			$files = array_reverse(explode('|', substr($file, 8)));
-			$i = 0;
-			foreach ($files as $file) {
-				if ($file[0] === '"') {
-					$file = trim($file, '".');
-				} else {
-					$file = "'{$file}'";
-				}
-				$i++;
-				if ($i === count($files) && isset($_attr['extends_resource'])) {
-					$this->compileEndChild($compiler);
-				}
-				$this->compileInclude($compiler, $file);
-			}
-			if (!isset($_attr['extends_resource'])) {
-				$this->compileEndChild($compiler);
-			}
-		} else {
-			$this->compileEndChild($compiler, $_attr['file']);
-		}
+		$this->compileEndChild($compiler, $_attr['file']);
 		return '';
 	}
 
 	/**
 	 * Add code for inheritance endChild() method to end of template
@@ -104,44 +82,6 @@ class ExtendsTag extends Inheritance {
 			$compiler->getParser(),
 			'<?php $_smarty_tpl->getInheritance()->endChild($_smarty_tpl' .
 			(isset($template) ?	", {$template}, \$_smarty_current_dir" : '') . ");\n?>"
 		);
 	}
-
-	/**
-	 * Add code for including subtemplate to end of template
-	 *
-	 * @param \Smarty\Compiler\Template $compiler
-	 * @param string $template subtemplate name
-	 *
-	 * @throws \Smarty\CompilerException
-	 * @throws \Smarty\Exception
-	 */
-	private function compileInclude(\Smarty\Compiler\Template $compiler, $template) {
-		$compiler->getParser()->template_postfix[] = new \Smarty\ParseTree\Tag(
-			$compiler->getParser(),
-			$compiler->compileTag(
-				'include',
-				[
-					$template,
-					['scope' => 'parent'],
-				]
-			)
-		);
-	}
-
-	/**
-	 * Create source code for {extends} from source components array
-	 *
-	 * @param \Smarty\Template $template
-	 *
-	 * @return string
-	 */
-	public static function extendsSourceArrayCode(\Smarty\Template $template) {
-		$resources = [];
-		foreach ($template->getSource()->components as $source) {
-			$resources[] = $source->resource;
-		}
-		return $template->getLeftDelimiter() . 'extends file=\'extends:' . join('|', $resources) .
-			'\' extends_resource=true' . $template->getRightDelimiter();
-	}
 }
diff --git a/src/Compiler/Template.php b/src/Compiler/Template.php
index b775abba0..1d4aa2447 100644
--- a/src/Compiler/Template.php
+++ b/src/Compiler/Template.php
@@ -401,25 +401,41 @@ class Template extends BaseCompiler {
 						$this->template->getSource()->type,
 					];
 			}
 			// get template source
 			if (!empty($this->template->getSource()->components)) {
-				// we have array of inheritance templates by extends: resource
-				// generate corresponding source code sequence
-				$_content =
-					ExtendsTag::extendsSourceArrayCode($this->template);
+
+				$_compiled_code = '<?php $_smarty_tpl->getInheritance()->init($_smarty_tpl, true); ?>';
+
+				$i = 0;
+				$reversed_components = array_reverse($this->template->getSource()->components);
+				foreach ($reversed_components as $source) {
+					$i++;
+					if ($i === count($reversed_components)) {
+						$_compiled_code .= '<?php $_smarty_tpl->getInheritance()->endChild($_smarty_tpl); ?>';
+					}
+					$_compiled_code .= $this->compileTag(
+						'include',
+						[
+							var_export($source->resource, true),
+							['scope' => 'parent'],
+						]
+					);
+				}
+				$_compiled_code = $this->smarty->runPostFilters($_compiled_code, $this->template);
 			} else {
 				// get template source
 				$_content = $this->template->getSource()->getContent();
+				$_compiled_code = $this->smarty->runPostFilters(
+					$this->doCompile(
+						$this->smarty->runPreFilters($_content, $this->template),
+						true
+					),
+					$this->template
+				);
 			}
-			$_compiled_code = $this->smarty->runPostFilters(
-				$this->doCompile(
-					$this->smarty->runPreFilters($_content, $this->template),
-					true
-				),
-				$this->template
-			);
+
 		} catch (\Exception $e) {
 			if ($this->smarty->debugging) {
 				$this->smarty->getDebug()->end_compile($this->template);
 			}
 			$this->_tag_stack = [];
diff --git a/src/Smarty.php b/src/Smarty.php
index dbb917262..9c27d7a83 100644
--- a/src/Smarty.php
+++ b/src/Smarty.php
@@ -2209,7 +2209,16 @@ class Smarty extends \Smarty\TemplateBase {
 			$template->caching = $this->caching;
 		}
 		return $template;
 	}
 
+	/**
+	 * Sets if Smarty should check If-Modified-Since headers to determine cache validity.
+	 * @param bool $cache_modified_check
+	 * @return void
+	 */
+	public function setCacheModifiedCheck($cache_modified_check): void {
+		$this->cache_modified_check = (bool) $cache_modified_check;
+	}
+
 }
 
diff --git a/tests/UnitTests/TemplateSource/TagTests/BockExtend/CompileBlockExtendsTest.php b/tests/UnitTests/TemplateSource/TagTests/BockExtend/CompileBlockExtendsTest.php
index 1b0ee50f9..6f0a798f5 100644
--- a/tests/UnitTests/TemplateSource/TagTests/BockExtend/CompileBlockExtendsTest.php
+++ b/tests/UnitTests/TemplateSource/TagTests/BockExtend/CompileBlockExtendsTest.php
@@ -1191,10 +1191,40 @@ class CompileBlockExtendsTest extends PHPUnit_Smarty
                      array("A{nocache}{block name='a'}\n{\$foo}{/block}\n{/nocache}C", "A\nbar\nC", 'Var3', $i++),
                      array("A{nocache}\n{block name='a'}\n{\$foo}\n{/block}\n{/nocache}C", "A\n\nbar\n\nC", 'Var4', $i++),
         );
     }
 
-	public function testBlockWithAssign() {
-		$this->assertEquals('Captured content is: Content with lots of html here', $this->smarty->fetch('038_child.tpl'));
-	}
+    public function testBlockWithAssign() {
+        $this->assertEquals('Captured content is: Content with lots of html here', $this->smarty->fetch('038_child.tpl'));
+    }
+
+    /**
+     * Test escaping of file parameter
+     */
+    public function testEscaping()
+    {
+        $this->expectException(\Smarty\Exception::class);
+        $this->expectExceptionMessageMatches('/Unable to load.*/');
+        $this->assertEquals('hello world', $this->smarty->fetch('escaping.tpl'));
+    }
+
+    /**
+     * Test escaping of file parameter 2
+     */
+    public function testEscaping2()
+    {
+        $this->expectException(\Smarty\Exception::class);
+        $this->expectExceptionMessageMatches('/Unable to load.*/');
+        $this->assertEquals('hello world', $this->smarty->fetch('escaping2.tpl'));
+    }
+
+    /**
+     * Test escaping of file parameter 3
+     */
+    public function testEscaping3()
+    {
+        $this->expectException(\Smarty\Exception::class);
+        $this->expectExceptionMessageMatches('/Unable to load.*/');
+        $this->assertEquals('hello world', $this->smarty->fetch('escaping3.tpl'));
+    }
 
 }
diff --git a/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping.tpl b/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping.tpl
new file mode 100644
index 000000000..79c52e0aa
--- /dev/null
+++ b/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping.tpl
@@ -0,0 +1 @@
+{extends "extends:helloworld.tpl', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}
\ No newline at end of file
diff --git a/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping2.tpl b/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping2.tpl
new file mode 100644
index 000000000..d72a57188
--- /dev/null
+++ b/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping2.tpl
@@ -0,0 +1 @@
+{extends 'extends:"helloworld.tpl\', var_dump(shell_exec(\'ls\')), 1, 2, 3);}}?>'}
\ No newline at end of file
diff --git a/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping3.tpl b/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping3.tpl
new file mode 100644
index 000000000..96372c82a
--- /dev/null
+++ b/tests/UnitTests/TemplateSource/TagTests/BockExtend/templates/escaping3.tpl
@@ -0,0 +1 @@
+{extends file='extends:"helloworld.tpl'|cat:"', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}
\ No newline at end of file
diff --git a/tests/UnitTests/TemplateSource/TagTests/Include/CompileIncludeTest.php b/tests/UnitTests/TemplateSource/TagTests/Include/CompileIncludeTest.php
index 8a7cb78e5..bd6eeaf40 100644
--- a/tests/UnitTests/TemplateSource/TagTests/Include/CompileIncludeTest.php
+++ b/tests/UnitTests/TemplateSource/TagTests/Include/CompileIncludeTest.php
@@ -80,10 +80,22 @@ class CompileIncludeTest extends PHPUnit_Smarty
         $this->smarty->setMergeCompiledIncludes($merge);
         $content = $this->smarty->fetch('test_include_001V3.tpl');
         $this->assertEquals('I1I2I3', $content, $text);
     }
 
+    /**
+     * test template name escaping
+     */
+    public function testIncludeFilenameEscaping()
+    {
+        $this->expectException(\Smarty\Exception::class);
+        $this->expectExceptionMessageMatches('/Unable to load.*/');
+        $tpl = $this->smarty->createTemplate('test_include_security.tpl');
+        $content = $this->smarty->fetch($tpl);
+        $this->assertEquals("hello world", $content);
+    }
+
     /**
      * test standard output
      *
      * 
      * 
diff --git a/tests/UnitTests/TemplateSource/TagTests/Include/templates/test_include_security.tpl b/tests/UnitTests/TemplateSource/TagTests/Include/templates/test_include_security.tpl
new file mode 100644
index 000000000..47d83bc14
--- /dev/null
+++ b/tests/UnitTests/TemplateSource/TagTests/Include/templates/test_include_security.tpl
@@ -0,0 +1 @@
+{include file="helloworld.tpl', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}
diff --git a/tests/UnitTests/TemplateSource/_Issues/419/ExtendsIssue419Test.php b/tests/UnitTests/TemplateSource/_Issues/419/ExtendsIssue419Test.php
index 2079fc0a6..9f6d81643 100644
--- a/tests/UnitTests/TemplateSource/_Issues/419/ExtendsIssue419Test.php
+++ b/tests/UnitTests/TemplateSource/_Issues/419/ExtendsIssue419Test.php
@@ -30,6 +30,13 @@ class ExtendsIssue419Test extends PHPUnit_Smarty
         $this->smarty->setLeftDelimiter('{{');
         $this->smarty->setRightDelimiter('}}');
         $this->assertEquals('child', $this->smarty->fetch('extends:001_parent.tpl|001_child.tpl'));
     }
 
+    public function testextendsSecurity()
+    {
+        $this->expectException(\Smarty\Exception::class);
+        $this->expectExceptionMessageMatches('/Unable to load.*/');
+        $this->assertEquals('child', $this->smarty->fetch('string:{include "001_parent.tpl\', var_dump(shell_exec(\'ls\')), 1, 2, 3);}}?>"}'));
+    }
+
 }

Vulnerable code: 	protected $optional_attributes = ['extends_resource'];
		$file = trim($_attr['file'], '\'"');
		if (strlen($file) > 8 && substr($file, 0, 8) === 'extends:') {
			// generate code for each template
			$files = array_reverse(explode('|', substr($file, 8)));
			$i = 0;
			foreach ($files as $file) {
				if ($file[0] === '"') {
					$file = trim($file, '".');
				} else {
					$file = "'{$file}'";
				}
				$i++;
				if ($i === count($files) && isset($_attr['extends_resource'])) {
					$this->compileEndChild($compiler);
				}
				$this->compileInclude($compiler, $file);
			}
			if (!isset($_attr['extends_resource'])) {
				$this->compileEndChild($compiler);
			}
		} else {
			$this->compileEndChild($compiler, $_attr['file']);
		}

	/**
	 * Add code for including subtemplate to end of template
	 *
	 * @param \Smarty\Compiler\Template $compiler
	 * @param string $template subtemplate name
	 *
	 * @throws \Smarty\CompilerException
	 * @throws \Smarty\Exception
	 */
	private function compileInclude(\Smarty\Compiler\Template $compiler, $template) {
		$compiler->getParser()->template_postfix[] = new \Smarty\ParseTree\Tag(
			$compiler->getParser(),
			$compiler->compileTag(
				'include',
				[
					$template,
					['scope' => 'parent'],
				]
			)
		);
	}

	/**
	 * Create source code for {extends} from source components array
	 *
	 * @param \Smarty\Template $template
	 *
	 * @return string
	 */
	public static function extendsSourceArrayCode(\Smarty\Template $template) {
		$resources = [];
		foreach ($template->getSource()->components as $source) {
			$resources[] = $source->resource;
		}
		return $template->getLeftDelimiter() . 'extends file=\'extends:' . join('|', $resources) .
			'\' extends_resource=true' . $template->getRightDelimiter();
	}
				// we have array of inheritance templates by extends: resource
				// generate corresponding source code sequence
				$_content =
					ExtendsTag::extendsSourceArrayCode($this->template);
			$_compiled_code = $this->smarty->runPostFilters(
				$this->doCompile(
					$this->smarty->runPreFilters($_content, $this->template),
					true
				),
				$this->template
			);
	public function testBlockWithAssign() {
		$this->assertEquals('Captured content is: Content with lots of html here', $this->smarty->fetch('038_child.tpl'));
	}
Fixed code: - Fixed a code injection vulnerability in extends-tag. This addresses CVE-2024-35226.
- Added `$smarty->setCacheModifiedCheck()` setter for cache_modified_check
	protected $optional_attributes = [];
		$this->compileEndChild($compiler, $_attr['file']);

				$_compiled_code = '<?php $_smarty_tpl->getInheritance()->init($_smarty_tpl, true); ?>';

				$i = 0;
				$reversed_components = array_reverse($this->template->getSource()->components);
				foreach ($reversed_components as $source) {
					$i++;
					if ($i === count($reversed_components)) {
						$_compiled_code .= '<?php $_smarty_tpl->getInheritance()->endChild($_smarty_tpl); ?>';
					}
					$_compiled_code .= $this->compileTag(
						'include',
						[
							var_export($source->resource, true),
							['scope' => 'parent'],
						]
					);
				}
				$_compiled_code = $this->smarty->runPostFilters($_compiled_code, $this->template);
				$_compiled_code = $this->smarty->runPostFilters(
					$this->doCompile(
						$this->smarty->runPreFilters($_content, $this->template),
						true
					),
					$this->template
				);

	/**
	 * Sets if Smarty should check If-Modified-Since headers to determine cache validity.
	 * @param bool $cache_modified_check
	 * @return void
	 */
	public function setCacheModifiedCheck($cache_modified_check): void {
		$this->cache_modified_check = (bool) $cache_modified_check;
	}

    public function testBlockWithAssign() {
        $this->assertEquals('Captured content is: Content with lots of html here', $this->smarty->fetch('038_child.tpl'));
    }

    /**
     * Test escaping of file parameter
     */
    public function testEscaping()
    {
        $this->expectException(\Smarty\Exception::class);
        $this->expectExceptionMessageMatches('/Unable to load.*/');
        $this->assertEquals('hello world', $this->smarty->fetch('escaping.tpl'));
    }

    /**
     * Test escaping of file parameter 2
     */
    public function testEscaping2()
    {
        $this->expectException(\Smarty\Exception::class);
        $this->expectExceptionMessageMatches('/Unable to load.*/');
        $this->assertEquals('hello world', $this->smarty->fetch('escaping2.tpl'));
    }

    /**
     * Test escaping of file parameter 3
     */
    public function testEscaping3()
    {
        $this->expectException(\Smarty\Exception::class);
        $this->expectExceptionMessageMatches('/Unable to load.*/');
        $this->assertEquals('hello world', $this->smarty->fetch('escaping3.tpl'));
    }
{extends "extends:helloworld.tpl', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}
{extends 'extends:"helloworld.tpl\', var_dump(shell_exec(\'ls\')), 1, 2, 3);}}?>'}
{extends file='extends:"helloworld.tpl'|cat:"', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}
    /**
     * test template name escaping
     */
    public function testIncludeFilenameEscaping()
    {
        $this->expectException(\Smarty\Exception::class);
        $this->expectExceptionMessageMatches('/Unable to load.*/');
        $tpl = $this->smarty->createTemplate('test_include_security.tpl');
        $content = $this->smarty->fetch($tpl);
        $this->assertEquals("hello world", $content);
    }

{include file="helloworld.tpl', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}
    public function testextendsSecurity()
    {
        $this->expectException(\Smarty\Exception::class);
        $this->expectExceptionMessageMatches('/Unable to load.*/');
        $this->assertEquals('child', $this->smarty->fetch('string:{include "001_parent.tpl\', var_dump(shell_exec(\'ls\')), 1, 2, 3);}}?>"}'));
    }