Package: release.debian.org Severity: normal Tags: trixie X-Debbugs-Cc: [email protected] Control: affects -1 + src:smarty4 User: [email protected] Usertags: pu
Please accept upload of smarty4 4.5.5-1+deb13u1 to Debian trixie. [ Reason ] Security fixes: CVE-2026-62992: Fix resolving symbolic links in Security::_checkDir() before validating that a requested path lies within a configured secure directory.(Closes: #1145020). CVE-2026-62993: Fix validating the initial remote URL against trusted_uri under all circumstances when a security policy is active. (Closes: #1150507). CVE-2026-82531: Fix code injection vulnerability. (Closes: #1150192). [ Impact ] Security issues remain unfixed. [ Tests ] Tested using GOsa² in Debian Edu. [ Risks ] Impacted packages in Debian: - gosa (et al.) - postfixadmin - slbackup-php [ Checklist ] [x] *all* changes are documented in the d/changelog [x] I reviewed all changes and I approve them [x] attach debdiff against the package in (old)stable [x] the issue is verified as fixed in unstable [ Changes ] +smarty4 (4.5.5-1+deb13u1) trixie; urgency=medium + + * debian/patches: + + Add CVE-2026-62992+3.patch. + CVE-2026-62992: Fix resolving symbolic links in Security::_checkDir() + before validating that a requested path lies within a configured secure + directory.(Closes: #1145020). + CVE-2026-62993: Fix validating the initial remote URL against trusted_uri + under all circumstances when a security policy is active. (Closes: + #1150507). + + Add CVE-2026-82531.patch. + CVE-2026-82531: Fix code injection vulnerability. (Closes: #1150192). + + -- Mike Gabriel <[email protected]> Tue, 03 Nov 2026 19:12:58 +0100 [ Other info ] Security issues were marked as no-dsa by Debian's security team.
diff -Nru smarty4-4.5.5/debian/changelog smarty4-4.5.5/debian/changelog --- smarty4-4.5.5/debian/changelog 2025-03-23 15:58:57.000000000 +0100 +++ smarty4-4.5.5/debian/changelog 2026-11-03 19:12:58.000000000 +0100 @@ -1,3 +1,18 @@ +smarty4 (4.5.5-1+deb13u1) trixie; urgency=medium + + * debian/patches: + + Add CVE-2026-62992+3.patch. + CVE-2026-62992: Fix resolving symbolic links in Security::_checkDir() + before validating that a requested path lies within a configured secure + directory.(Closes: #1145020). + CVE-2026-62993: Fix validating the initial remote URL against trusted_uri + under all circumstances when a security policy is active. (Closes: + #1150507). + + Add CVE-2026-82531.patch. + CVE-2026-82531: Fix code injection vulnerability. (Closes: #1150192). + + -- Mike Gabriel <[email protected]> Tue, 03 Nov 2026 19:12:58 +0100 + smarty4 (4.5.5-1) unstable; urgency=medium * New upstream release. diff -Nru smarty4-4.5.5/debian/patches/CVE-2026-62992+3.patch smarty4-4.5.5/debian/patches/CVE-2026-62992+3.patch --- smarty4-4.5.5/debian/patches/CVE-2026-62992+3.patch 1970-01-01 01:00:00.000000000 +0100 +++ smarty4-4.5.5/debian/patches/CVE-2026-62992+3.patch 2026-11-03 19:04:15.000000000 +0100 @@ -0,0 +1,153 @@ +From a1ccdb0518021a559b4066c37b76a42c86bbce90 Mon Sep 17 00:00:00 2001 +From: Simon Wisselink <[email protected]> +Date: Mon, 29 Jun 2026 10:48:33 +0200 +Subject: [PATCH] Port security fixes from v5.8.2 to Smarty v4. (#1194) + +--- + changelog/security-fetch-ssrf-redirect.md | 1 + + changelog/security-html-image-escaping.md | 1 + + changelog/security-symlink-path-traversal.md | 1 + + libs/plugins/function.fetch.php | 19 +++- + libs/plugins/function.html_image.php | 11 ++- + libs/plugins/function.html_select_date.php | 10 +- + libs/sysplugins/smarty_security.php | 26 ++++- + .../UnitTests/SecurityTests/SecurityTest.php | 81 ++++++++++++++++ + .../PluginFunctionFetchTest.php | 95 +++++++++++++++++++ + .../PluginFunctionHtmlImageTest.php | 84 ++++++++++++++++ + .../PluginFunctionHtmlSelectDateTest.php | 13 +++ + 11 files changed, 333 insertions(+), 9 deletions(-) + create mode 100644 changelog/security-fetch-ssrf-redirect.md + create mode 100644 changelog/security-html-image-escaping.md + create mode 100644 changelog/security-symlink-path-traversal.md + create mode 100644 tests/UnitTests/TemplateSource/TagTests/PluginFunction/PluginFunctionHtmlImageTest.php + +--- /dev/null ++++ b/changelog/security-fetch-ssrf-redirect.md +@@ -0,0 +1 @@ ++- Security: `{fetch}` no longer follows HTTP redirects for remote resources while a security policy is active, preventing an open redirect on a trusted host from bypassing `trusted_uri` (CWE-918 server-side request forgery) +--- /dev/null ++++ b/changelog/security-html-image-escaping.md +@@ -0,0 +1 @@ ++- Security: `{html_image}` now escapes the `file`, `path_prefix`, `href`/`link`, `width` and `height` attributes (it already escaped `alt` and pass-through attributes), and `{html_select_date}` casts `day_size`/`month_size`/`year_size` to int (matching `{html_select_time}`), preventing untrusted values passed into these attributes from breaking out of the generated HTML (CWE-79) +--- /dev/null ++++ b/changelog/security-symlink-path-traversal.md +@@ -0,0 +1 @@ ++- Security: prevent symlinks inside a trusted `secure_dir`/template directory from being used to read files outside of it (CWE-22 path traversal), affecting `{include}` and `{fetch}` of local files +--- a/libs/plugins/function.fetch.php ++++ b/libs/plugins/function.fetch.php +@@ -191,7 +191,24 @@ + return; + } + } else { +- $content = @file_get_contents($params[ 'file' ]); ++ if ($protocol && isset($template->smarty->security_policy)) { ++ // Remote resource (e.g. https://) reached through file_get_contents(). ++ // isTrustedUri() only validates the initial URL, but file_get_contents() ++ // follows redirects by default, so an open redirect on an otherwise ++ // trusted host could be used to reach a non-trusted target (SSRF). ++ // Disable redirect-following while a security policy is in effect. ++ $context = stream_context_create( ++ array( ++ 'http' => array( ++ 'follow_location' => 0, ++ 'max_redirects' => 1, ++ ), ++ ) ++ ); ++ $content = @file_get_contents($params[ 'file' ], false, $context); ++ } else { ++ $content = @file_get_contents($params[ 'file' ]); ++ } + if ($content === false) { + throw new SmartyException("{fetch} cannot read resource '" . $params[ 'file' ] . "'"); + } +--- a/libs/plugins/function.html_image.php ++++ b/libs/plugins/function.html_image.php +@@ -75,7 +75,7 @@ + break; + case 'link': + case 'href': +- $prefix = '<a href="' . $_val . '">'; ++ $prefix = '<a href="' . smarty_function_escape_special_chars($_val) . '">'; + $suffix = '</a>'; + break; + default: +@@ -153,6 +153,11 @@ + $width = round($width * $_resize); + $height = round($height * $_resize); + } +- return $prefix . '<img src="' . $path_prefix . $file . '" alt="' . $alt . '" width="' . $width . '" height="' . +- $height . '"' . $extra . ' />' . $suffix; ++ // $alt and the pass-through attributes ($extra) are already escaped above; ++ // escape the remaining value-context params at output time so untrusted ++ // values cannot break out of the attribute (CWE-79). The unescaped $file/ ++ // $width/$height are still used for getimagesize()/DPI math above. ++ return $prefix . '<img src="' . smarty_function_escape_special_chars($path_prefix . $file) . '" alt="' . $alt ++ . '" width="' . smarty_function_escape_special_chars($width) . '" height="' ++ . smarty_function_escape_special_chars($height) . '"' . $extra . ' />' . $suffix; + } +--- a/libs/plugins/function.html_select_date.php ++++ b/libs/plugins/function.html_select_date.php +@@ -131,9 +131,6 @@ + case 'day_value_format': + case 'month_format': + case 'month_value_format': +- case 'day_size': +- case 'month_size': +- case 'year_size': + case 'all_extra': + case 'day_extra': + case 'month_extra': +@@ -151,6 +148,13 @@ + case 'year_id': + $$_key = (string)$_value; + break; ++ case 'day_size': ++ case 'month_size': ++ case 'year_size': ++ // numeric HTML size attribute; cast to int (consistent with ++ // html_select_time) so it cannot break out of size="…" (CWE-79) ++ $$_key = (int)$_value; ++ break; + case 'display_days': + case 'display_months': + case 'display_years': +--- a/libs/sysplugins/smarty_security.php ++++ b/libs/sysplugins/smarty_security.php +@@ -590,12 +590,34 @@ + */ + private function _checkDir($filepath, $dirs) + { +- $directory = dirname($this->smarty->_realpath($filepath, true)) . DIRECTORY_SEPARATOR; ++ // Resolve the canonical, symlink-free path of the requested file so that ++ // a symlink located inside a trusted directory cannot be abused to read ++ // a file outside of it (CWE-22 path traversal). Smarty::_realpath() only ++ // normalizes the path as a string and does not follow symlinks, so we ++ // fall back to it only when the file does not yet exist on disk (e.g. ++ // config/cache paths that are validated before being written). ++ $realpath = @realpath($filepath); ++ $resolved = $realpath !== false ? $realpath : $this->smarty->_realpath($filepath, true); ++ $directory = dirname($resolved) . DIRECTORY_SEPARATOR; ++ ++ // Canonicalize the trusted directories the same way. This keeps ++ // legitimate symlinked deployment paths working (e.g. a Capistrano-style ++ // "current" release symlink, or macOS' /var -> /private/var): both the ++ // file and the trusted directories are compared after symlinks have been ++ // resolved. ++ $trusted = array(); ++ foreach ($dirs as $dir => $unused) { ++ $trusted[ $dir ] = true; ++ if (($dirRealpath = @realpath($dir)) !== false) { ++ $trusted[ rtrim($dirRealpath, '\\/') . DIRECTORY_SEPARATOR ] = true; ++ } ++ } ++ + $_directory = array(); + if (!preg_match('#[\\\\/][.][.][\\\\/]#', $directory)) { + while (true) { + // test if the directory is trusted +- if (isset($dirs[ $directory ])) { ++ if (isset($trusted[ $directory ])) { + return $_directory; + } + // abort if we've reached root diff -Nru smarty4-4.5.5/debian/patches/CVE-2026-82531.patch smarty4-4.5.5/debian/patches/CVE-2026-82531.patch --- smarty4-4.5.5/debian/patches/CVE-2026-82531.patch 1970-01-01 01:00:00.000000000 +0100 +++ smarty4-4.5.5/debian/patches/CVE-2026-82531.patch 2026-11-03 18:59:27.000000000 +0100 @@ -0,0 +1,212 @@ +From 1cba51cb813563eb61d963c83d28cd59f26b858d Mon Sep 17 00:00:00 2001 +From: Simon Wisselink <[email protected]> +Date: Sun, 4 Oct 2026 23:26:08 +0200 +Subject: [PATCH] Security: prevent nocache-marker forgery leading to + cache-poisoning RCE + +A template rendered through the extends:/inheritance path produces a +top-level unifunc whose nocache_hash is never restored, so on a fresh +process that reloads the compiled file from disk it stays null. Folded +into the cache-split regex in Smarty_Internal_Runtime_UpdateCache, that +null yields an empty alternative "(realhash|)", letting attacker-controlled +output forge a /*%%SmartyNocache:%%*/ marker whose raw content is copied +verbatim into the generated cache file and executed as PHP on the next +include() (CWE-94). + +Only fold in the top-level nocache hash when it is set (avoiding both the +empty alternative and the PHP 8.1+ null-array-offset deprecation), and +when no hashes remain treat the whole buffer as ordinary output so forged +markers are PHP-tag neutralized like any other content. + +Adds a regression test that forges a marker from assigned data and +asserts the injected PHP never executes. +--- + CHANGELOG.md | 1 + + .../smarty_internal_runtime_updatecache.php | 46 ++++++--- + .../NocacheHashInjectionTest.php | 98 +++++++++++++++++++ + .../templates/nchi_base.tpl | 1 + + .../templates/nchi_over.tpl | 1 + + 5 files changed, 134 insertions(+), 13 deletions(-) + create mode 100644 tests/UnitTests/SecurityTests/NocacheHashInjection/NocacheHashInjectionTest.php + create mode 100644 tests/UnitTests/SecurityTests/NocacheHashInjection/templates/nchi_base.tpl + create mode 100644 tests/UnitTests/SecurityTests/NocacheHashInjection/templates/nchi_over.tpl + +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -5,6 +5,7 @@ + and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + ## [Unreleased] ++- Security: prevent attacker-controlled template output from forging a nocache marker that injects raw PHP into the generated cache file when a template is rendered through the `extends:`/inheritance path (CWE-94 code injection); the top-level unifunc's missing nocache hash no longer produces an empty alternative in the cache-split regex + + ## [4.5.5] - 2024-11-21 + +--- a/libs/sysplugins/smarty_internal_runtime_updatecache.php ++++ b/libs/sysplugins/smarty_internal_runtime_updatecache.php +@@ -72,22 +72,42 @@ + $php_pattern = '/(<%|%>|<\?php|<\?|\?>|<script\s+language\s*=\s*[\"\']?\s*php\s*[\"\']?\s*>)/'; + $content = ob_get_clean(); + $hash_array = $cached->hashes; +- $hash_array[ $_template->compiled->nocache_hash ] = true; +- $hash_array = array_keys($hash_array); +- $nocache_hash = '(' . implode('|', $hash_array) . ')'; ++ // Only fold in the top-level nocache hash when it is actually set. A template ++ // compiled through the inheritance/component path produces a top-level unifunc ++ // that never restores its nocache_hash, leaving it null here. Using that null as ++ // an array offset is deprecated (PHP 8.1+), and an empty hash would produce an ++ // empty alternative "(realhash|)" in the pattern below, letting attacker-controlled ++ // output forge a nocache marker whose raw content is copied verbatim into the cache ++ // file (skipping PHP-tag neutralization) and executed as PHP on the next include ++ // (CWE-94 code injection). ++ if ((string) $_template->compiled->nocache_hash !== '') { ++ $hash_array[ $_template->compiled->nocache_hash ] = true; ++ } ++ // Defensive: never allow an empty hash (hence an empty regex alternative). ++ $hash_array = array_filter(array_keys($hash_array), function ($hash) { ++ return (string) $hash !== ''; ++ }); + $_template->cached->has_nocache_code = false; +- // get text between non-cached items +- $cache_split = +- preg_split( ++ if ($hash_array === array()) { ++ // No known nocache hashes: there is no legitimate nocache content to extract, ++ // so treat the whole buffer as ordinary output (PHP tags are neutralized below). ++ $cache_split = array($content); ++ $cache_parts = array(); ++ } else { ++ $nocache_hash = '(' . implode('|', $hash_array) . ')'; ++ // get text between non-cached items ++ $cache_split = ++ preg_split( ++ "!/\*%%SmartyNocache:{$nocache_hash}%%\*\/(.+?)/\*/%%SmartyNocache:{$nocache_hash}%%\*/!s", ++ $content ++ ); ++ // get non-cached items ++ preg_match_all( + "!/\*%%SmartyNocache:{$nocache_hash}%%\*\/(.+?)/\*/%%SmartyNocache:{$nocache_hash}%%\*/!s", +- $content ++ $content, ++ $cache_parts + ); +- // get non-cached items +- preg_match_all( +- "!/\*%%SmartyNocache:{$nocache_hash}%%\*\/(.+?)/\*/%%SmartyNocache:{$nocache_hash}%%\*/!s", +- $content, +- $cache_parts +- ); ++ } + $content = ''; + // loop over items, stitch back together + foreach ($cache_split as $curr_idx => $curr_split) { +--- /dev/null ++++ b/tests/UnitTests/SecurityTests/NocacheHashInjection/NocacheHashInjectionTest.php +@@ -0,0 +1,98 @@ ++<?php ++/** ++ * Regression test for the nocache-hash cache-poisoning RCE. ++ * ++ * A template rendered through the extends:/inheritance path produces a top-level ++ * unifunc whose nocache_hash is never restored, so on a fresh process that reloads ++ * the compiled file from disk it stays null. Folded into the cache-split regex in ++ * Smarty_Internal_Runtime_UpdateCache::removeNoCacheHash(), that null yields an ++ * empty regex alternative "(realhash|)", letting attacker-controlled output forge a ++ * nocache marker whose raw content is copied verbatim into the generated cache file ++ * and executed as PHP on the next include() (CWE-94). ++ * ++ * @runTestsInSeparateProcess ++ * @preserveGlobalState disabled ++ */ ++class NocacheHashInjectionTest extends PHPUnit_Smarty ++{ ++ /** ++ * @var string absolute path of the file the injected PHP would create ++ */ ++ private $sentinel; ++ ++ public function setUp(): void ++ { ++ $this->setUpSmarty(__DIR__); ++ $this->smarty->setTemplateDir(__DIR__ . '/templates'); ++ $this->sentinel = $this->smarty->getCacheDir() . 'nocache_hash_pwned.txt'; ++ @unlink($this->sentinel); ++ $this->cleanCompileDir(); ++ $this->cleanCacheDir(); ++ } ++ ++ public function tearDown(): void ++ { ++ @unlink($this->sentinel); ++ parent::tearDown(); ++ } ++ ++ /** ++ * Build a fresh, independently configured Smarty instance sharing the on-disk ++ * compile/cache/template dirs. A new instance reloads the compiled template ++ * from disk, reproducing the "fresh process" precondition under which the ++ * top-level inheritance unifunc leaves nocache_hash null. ++ * ++ * @return Smarty ++ */ ++ private function freshSmarty() ++ { ++ $smarty = new Smarty(); ++ $smarty->setCompileDir($this->smarty->getCompileDir()); ++ $smarty->setCacheDir($this->smarty->getCacheDir()); ++ $smarty->setTemplateDir($this->smarty->getTemplateDir()); ++ $smarty->caching = Smarty::CACHING_LIFETIME_CURRENT; ++ $smarty->compile_check = false; ++ return $smarty; ++ } ++ ++ /** ++ * An attacker-controlled assigned value that forges a nocache marker with an ++ * empty hash must never be treated as a real nocache section: its PHP tags ++ * must be neutralized like any other output, not copied verbatim into cache. ++ */ ++ public function testForgedNocacheMarkerIsNotExecuted() ++ { ++ $payload = '/*%%SmartyNocache:%%*/' ++ . '<?php file_put_contents(' . var_export($this->sentinel, true) . ', "PWNED"); ?>' ++ . '/*/%%SmartyNocache:%%*/'; ++ ++ // Phase 1: benign render compiles the templates and writes the initial cache. ++ $s1 = $this->freshSmarty(); ++ $s1->assign('name', 'world'); ++ $this->assertStringContainsString('Hello world!', $s1->fetch('extends:nchi_base.tpl|nchi_over.tpl')); ++ $this->assertFileNotExists($this->sentinel); ++ ++ // Cache evicted (routine TTL expiry / cache clear) while compiled files remain. ++ $this->cleanCacheDir(); ++ ++ // Phase 2: a fresh instance reloads the compiled file from disk (null ++ // nocache_hash) and regenerates the cache using attacker-controlled data. ++ $s2 = $this->freshSmarty(); ++ $s2->assign('name', $payload); ++ $s2->fetch('extends:nchi_base.tpl|nchi_over.tpl'); ++ $this->assertFileNotExists( ++ $this->sentinel, ++ 'Forged nocache marker executed injected PHP while (re)writing the cache file' ++ ); ++ ++ // Phase 3: the (would-be poisoned) cache is served via a pure include() on a ++ // later, benign request. The injected PHP must not run then either. ++ $s3 = $this->freshSmarty(); ++ $s3->assign('name', 'benign'); ++ $s3->fetch('extends:nchi_base.tpl|nchi_over.tpl'); ++ $this->assertFileNotExists( ++ $this->sentinel, ++ 'Poisoned cache file executed injected PHP when included on a later request' ++ ); ++ } ++} +--- /dev/null ++++ b/tests/UnitTests/SecurityTests/NocacheHashInjection/templates/nchi_base.tpl +@@ -0,0 +1 @@ ++{block name=body}default{/block} +\ No newline at end of file +--- /dev/null ++++ b/tests/UnitTests/SecurityTests/NocacheHashInjection/templates/nchi_over.tpl +@@ -0,0 +1 @@ ++{block name=body}Hello {$name}!{/block} +\ No newline at end of file diff -Nru smarty4-4.5.5/debian/patches/series smarty4-4.5.5/debian/patches/series --- smarty4-4.5.5/debian/patches/series 1970-01-01 01:00:00.000000000 +0100 +++ smarty4-4.5.5/debian/patches/series 2026-11-03 19:09:50.000000000 +0100 @@ -0,0 +1,2 @@ +CVE-2026-82531.patch +CVE-2026-62992+3.patch

