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

Reply via email to