Version in base suite: 4.5.5-1 Base version: smarty4_4.5.5-1 Target version: smarty4_4.5.5-1+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/s/smarty4/smarty4_4.5.5-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/s/smarty4/smarty4_4.5.5-1+deb13u1.dsc changelog | 15 ++ patches/CVE-2026-62992+3.patch | 153 +++++++++++++++++++++++++++++ patches/CVE-2026-82531.patch | 212 +++++++++++++++++++++++++++++++++++++++++ patches/series | 2 4 files changed, 382 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp9rr7r0cr/smarty4_4.5.5-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp9rr7r0cr/smarty4_4.5.5-1+deb13u1.dsc: no acceptable signature found diff -Nru smarty4-4.5.5/debian/changelog smarty4-4.5.5/debian/changelog --- smarty4-4.5.5/debian/changelog 2025-03-23 14:58:57.000000000 +0000 +++ smarty4-4.5.5/debian/changelog 2026-11-03 18:12:58.000000000 +0000 @@ -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 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 00:00:00.000000000 +0000 +++ smarty4-4.5.5/debian/patches/CVE-2026-62992+3.patch 2026-11-03 18:04:15.000000000 +0000 @@ -0,0 +1,153 @@ +From a1ccdb0518021a559b4066c37b76a42c86bbce90 Mon Sep 17 00:00:00 2001 +From: Simon Wisselink +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 = ''; ++ $prefix = ''; + $suffix = ''; + break; + default: +@@ -153,6 +153,11 @@ + $width = round($width * $_resize); + $height = round($height * $_resize); + } +- return $prefix . '' . $alt . '' . $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 . '' . $alt
++           . '' . $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 00:00:00.000000000 +0000 +++ smarty4-4.5.5/debian/patches/CVE-2026-82531.patch 2026-11-03 17:59:27.000000000 +0000 @@ -0,0 +1,212 @@ +From 1cba51cb813563eb61d963c83d28cd59f26b858d Mon Sep 17 00:00:00 2001 +From: Simon Wisselink +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|<\?|\?>|)/'; + $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 @@ ++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:%%*/' ++ . '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 00:00:00.000000000 +0000 +++ smarty4-4.5.5/debian/patches/series 2026-11-03 18:09:50.000000000 +0000 @@ -0,0 +1,2 @@ +CVE-2026-82531.patch +CVE-2026-62992+3.patch