CVE-2024-35226 (PHP, published 2024-05-28T21:16Z)
Description: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/smarty-php/smarty
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 message: Merge pull request from GHSA-4rmg-292m-wg3w



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);}}?>"}'));
    }