CVE-2024-35226 (published 2024-05-28T21:16Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/smarty-php/smarty
commit_details: repo_url: https://github.com/smarty-php/smarty
author: Simon Wisselink
author_date: 2024-05-28 22:44:30+02:00
msg: Merge pull request from GHSA-4rmg-292m-wg3w
repository: repo_url: https://github.com/smarty-php/smarty
repo_name: smarty-php/smarty
description: Smarty is a template engine for PHP, facilitating the separation of presentation (HTML/CSS) from application logic.
repo_language: PHP
owner: smarty-php
file_changes: filename: GHSA-4rmg-292m-wg3w.md
change_type: ModificationType.ADD
diff: @@ -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

programming_language: SQL
filename: ExtendsTag.php
change_type: ModificationType.MODIFY
diff: @@ -32,7 +32,7 @@ class ExtendsTag extends Inheritance {
 	 *
 	 * @var array
 	 */
-	protected $optional_attributes = ['extends_resource'];
+	protected $optional_attributes = [];
 
 	/**
 	 * Attribute definition: Overwrites base class.
@@ -64,29 +64,7 @@ class ExtendsTag extends Inheritance {
 		}
 		// 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 '';
 	}
 
@@ -106,42 +84,4 @@ class ExtendsTag extends Inheritance {
 			(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();
-	}
 }

programming_language: PHP
filename: Template.php
change_type: ModificationType.MODIFY
diff: @@ -403,21 +403,37 @@ class Template extends BaseCompiler {
 			}
 			// 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);

programming_language: PHP
filename: Smarty.php
change_type: ModificationType.MODIFY
diff: @@ -2211,5 +2211,14 @@ class Smarty extends \Smarty\TemplateBase {
 		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;
+	}
+
 }
 

programming_language: PHP
filename: CompileBlockExtendsTest.php
change_type: ModificationType.MODIFY
diff: @@ -1193,8 +1193,38 @@ class CompileBlockExtendsTest extends PHPUnit_Smarty
         );
     }
 
-	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'));
+    }
 
 }

programming_language: PHP
filename: escaping.tpl
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+{extends "extends:helloworld.tpl', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}
\ No newline at end of file

programming_language: SQL
filename: escaping2.tpl
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+{extends 'extends:"helloworld.tpl\', var_dump(shell_exec(\'ls\')), 1, 2, 3);}}?>'}
\ No newline at end of file

programming_language: SQL
filename: escaping3.tpl
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+{extends file='extends:"helloworld.tpl'|cat:"', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}
\ No newline at end of file

programming_language: SQL
filename: CompileIncludeTest.php
change_type: ModificationType.MODIFY
diff: @@ -82,6 +82,18 @@ class CompileIncludeTest extends PHPUnit_Smarty
         $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
      *

programming_language: PHP
filename: test_include_security.tpl
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+{include file="helloworld.tpl', var_dump(shell_exec('ls')), 1, 2, 3);}}?>"}

programming_language: SQL
filename: ExtendsIssue419Test.php
change_type: ModificationType.MODIFY
diff: @@ -32,4 +32,11 @@ class ExtendsIssue419Test extends PHPUnit_Smarty
         $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);}}?>"}'));
+    }
+
 }

programming_language: PHP