CVE-2018-13982: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Smarty_Security::isTrustedResourceDir() in Smarty before 3.1.33 is prone to a path traversal vulnerability due to insufficient template code sanitization. This allows attackers controlling the executed template code to bypass the trusted directory security restriction and read arbitrary files.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 7.5
Published: 2018-09-18T21:29Z
Repository: https://github.com/smarty-php/smarty
Language: PHP
Files changed: change_log.txt, libs/Smarty.class.php, libs/sysplugins/smarty_security.php
Commit date: 2018-04-24 10:38:18 +0200
Commit message: - bugfix  possible Security Vulnerability in Smarty_Security class.

Diff: diff --git a/change_log.txt b/change_log.txt
index f92af8405..a3ebcc36b 100644
--- a/change_log.txt
+++ b/change_log.txt
@@ -1,6 +1,9 @@
 ﻿===== 3.1.32 - dev ===
+24.04.2018
+ - bugfix  possible Security Vulnerability in Smarty_Security class.
+
 26.03.2018
  - bugfix plugins may not be loaded if {function} or {block} tags are executed in nocache mode
    https://github.com/smarty-php/smarty/issues/371
 
 26.03.2018
diff --git a/libs/Smarty.class.php b/libs/Smarty.class.php
index d9e96703b..aba244415 100644
--- a/libs/Smarty.class.php
+++ b/libs/Smarty.class.php
@@ -110,11 +110,11 @@ require_once SMARTY_SYSPLUGINS_DIR . 'smarty_internal_resource_file.php';
 class Smarty extends Smarty_Internal_TemplateBase
 {
     /**
      * smarty version
      */
-    const SMARTY_VERSION = '3.1.32-dev-45';
+    const SMARTY_VERSION = '3.1.32-dev-46';
     /**
      * define variable scopes
      */
     const SCOPE_LOCAL    = 1;
     const SCOPE_PARENT   = 2;
@@ -1040,13 +1040,19 @@ class Smarty extends Smarty_Internal_TemplateBase
      *
      * @return string
      */
     public function _realpath($path, $realpath = null)
     {
-        $nds = DIRECTORY_SEPARATOR === '/' ? '\\' : '/';
+        static $nds = array('/' => '\\', '\\' => '/');
+        static $sepDotsep = DIRECTORY_SEPARATOR . '.' . DIRECTORY_SEPARATOR;
+        static $sepDot = DIRECTORY_SEPARATOR . '.';
+        static $sepSep = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR;
         // normalize DIRECTORY_SEPARATOR
-        $path = str_replace($nds, DIRECTORY_SEPARATOR, $path);
+        $path = str_replace(array($nds[DIRECTORY_SEPARATOR], $sepDotsep), DIRECTORY_SEPARATOR, $path);
+        if (strpos($path,$sepDot) === false && (($realpath === false && $path[0] === '.') || $realpath === null) && $path[0] !== '\\') {
+            return $path;
+        }
         preg_match('%^(?<root>(?:[[:alpha:]]:[\\\\]|/|[\\\\]{2}[[:alpha:]]+|[[:print:]]{2,}:[/]{2}|[\\\\])?)(?<path>(.*))$%u',
                    $path,
                    $parts);
         $path = $parts[ 'path' ];
         if ($parts[ 'root' ] === '\\') {
@@ -1054,29 +1060,29 @@ class Smarty extends Smarty_Internal_TemplateBase
         } else {
             if ($realpath !== null && !$parts[ 'root' ]) {
                 $path = getcwd() . DIRECTORY_SEPARATOR . $path;
             }
         }
-        // remove noop 'DIRECTORY_SEPARATOR DIRECTORY_SEPARATOR' and 'DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR' patterns
-        $path = preg_replace('#([\\\\/]([.]?[\\\\/])+)#u', DIRECTORY_SEPARATOR, $path);
+       // remove noop 'DIRECTORY_SEPARATOR DIRECTORY_SEPARATOR' and 'DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR' patterns
+        $path = str_replace(array($sepDotsep,$sepSep), DIRECTORY_SEPARATOR, $path);
         // resolve '..DIRECTORY_SEPARATOR' pattern, smallest first
         if (strpos($path, '..' . DIRECTORY_SEPARATOR) !== false &&
-            preg_match_all('#(([.]?[\\\\/])*([.][.])[\\\\/]([.]?[\\\\/])*)+#u', $path, $match)
+            preg_match_all('#[\\\\/]([.][.][\\\\/])+#u', $path, $match)
         ) {
             $counts = array();
             foreach ($match[ 0 ] as $m) {
                 $counts[] = (int)((strlen($m) - 1) / 3);
             }
             sort($counts);
             foreach ($counts as $count) {
-                $path = preg_replace('#(([\\\\/]([.]?[\\\\/])*[^\\\\/.]+){' . $count .
-                                     '}[\\\\/]([.]?[\\\\/])*([.][.][\\\\/]([.]?[\\\\/])*){' . $count . '})(?=[^.])#u',
+                $path = preg_replace('#([\\\\/]+[^\\\\/]+){' . $count .
+                                     '}[\\\\/]+([.][.][\\\\/]+){' . $count . '}#u',
                                      DIRECTORY_SEPARATOR,
                                      $path);
             }
         }
-        return $parts[ 'root' ] . $path;
+        return $realpath !== false ? $parts[ 'root' ] . $path : str_ireplace(getcwd(), '.', $parts[ 'root' ] . $path);
     }
 
     /**
      * Empty template objects cache
      */
diff --git a/libs/sysplugins/smarty_security.php b/libs/sysplugins/smarty_security.php
index e501cd825..126f6fb46 100644
--- a/libs/sysplugins/smarty_security.php
+++ b/libs/sysplugins/smarty_security.php
@@ -512,64 +512,43 @@ class Smarty_Security
      * @throws \SmartyException if directory is not trusted
      */
     public function isTrustedResourceDir($filepath, $isConfig = null)
     {
         if ($this->_include_path_status !== $this->smarty->use_include_path) {
-            foreach ($this->_include_dir as $directory) {
-                unset($this->_resource_dir[ $directory ]);
-            }
-            if ($this->smarty->use_include_path) {
-                $this->_include_dir = array();
-                $_dirs = $this->smarty->ext->_getIncludePath->getIncludePathDirs($this->smarty);
-                foreach ($_dirs as $directory) {
-                    $this->_include_dir[] = $directory;
-                    $this->_resource_dir[ $directory ] = true;
-                }
+            $_dir = $this->smarty->use_include_path ? $this->smarty->ext->_getIncludePath->getIncludePathDirs($this->smarty) : array();
+            if ($this->_include_dir !== $_dir) {
+                $this->_updateResourceDir($this->_include_dir, $_dir);
+                $this->_include_dir = $_dir;
             }
             $this->_include_path_status = $this->smarty->use_include_path;
         }
-        if ($isConfig !== true &&
-            (!isset($this->smarty->_cache[ 'template_dir_new' ]) || $this->smarty->_cache[ 'template_dir_new' ])
-        ) {
+        if ($isConfig !== true) {
             $_dir = $this->smarty->getTemplateDir();
             if ($this->_template_dir !== $_dir) {
-                foreach ($this->_template_dir as $directory) {
-                    unset($this->_resource_dir[ $directory ]);
-                }
-                foreach ($_dir as $directory) {
-                    $this->_resource_dir[ $directory ] = true;
-                }
+                $this->_updateResourceDir($this->_template_dir, $_dir);
                 $this->_template_dir = $_dir;
             }
-            $this->smarty->_cache[ 'template_dir_new' ] = false;
         }
-        if ($isConfig !== false &&
-            (!isset($this->smarty->_cache[ 'config_dir_new' ]) || $this->smarty->_cache[ 'config_dir_new' ])
-        ) {
+        if ($isConfig !== false) {
             $_dir = $this->smarty->getConfigDir();
             if ($this->_config_dir !== $_dir) {
-                foreach ($this->_config_dir as $directory) {
-                    unset($this->_resource_dir[ $directory ]);
-                }
-                foreach ($_dir as $directory) {
-                    $this->_resource_dir[ $directory ] = true;
-                }
+                $this->_updateResourceDir($this->_config_dir, $_dir);
                 $this->_config_dir = $_dir;
             }
-            $this->smarty->_cache[ 'config_dir_new' ] = false;
         }
-        if ($this->_secure_dir !== (array) $this->secure_dir) {
-            foreach ($this->_secure_dir as $directory) {
-                unset($this->_resource_dir[ $directory ]);
+        if ($this->_secure_dir !== $this->secure_dir) {
+            $this->secure_dir = (array)$this->secure_dir;
+            foreach($this->secure_dir as $k => $d) {
+                $this->secure_dir[$k] = $this->smarty->_realpath($d.DIRECTORY_SEPARATOR,true);
             }
-            foreach ((array) $this->secure_dir as $directory) {
-                $directory = $this->smarty->_realpath($directory . DIRECTORY_SEPARATOR, true);
-                $this->_resource_dir[ $directory ] = true;
-            }
-            $this->_secure_dir = (array) $this->secure_dir;
+            $this->_updateResourceDir($this->_secure_dir, $this->secure_dir);
+            $this->_secure_dir = $this->secure_dir;
+        }
+        $addPath =  $this->_checkDir($filepath, $this->_resource_dir);
+        if ($addPath !== false) {
+           $this->_resource_dir = array_merge($this->_resource_dir, $addPath);
         }
-        $this->_resource_dir = $this->_checkDir($filepath, $this->_resource_dir);
         return true;
     }
 
     /**
      * Check if URI (e.g. {fetch} or {html_image}) is trusted
@@ -620,44 +599,68 @@ class Smarty_Security
             foreach ((array) $this->trusted_dir as $directory) {
                 $directory = $this->smarty->_realpath($directory . DIRECTORY_SEPARATOR, true);
                 $this->_php_resource_dir[ $directory ] = true;
             }
         }
-
-        $this->_php_resource_dir =
-            $this->_checkDir($this->smarty->_realpath($filepath, true), $this->_php_resource_dir);
-        return true;
+        $addPath =  $this->_checkDir($filepath, $this->_php_resource_dir);
+        if ($addPath !== false) {
+           $this->_php_resource_dir = array_merge($this->_php_resource_dir, $addPath);
+        }
+         return true;
     }
 
+    /**
+     * Remove old directories and its sub folders, add new directories
+     *
+     * @param array $oldDir
+     * @param array $newDir
+     */
+    private function _updateResourceDir($oldDir, $newDir) {
+        foreach ($oldDir as $directory) {
+            $directory = $this->smarty->_realpath($directory, true);
+            $length = strlen($directory);
+            foreach ($this->_resource_dir as $dir) {
+                if (substr($dir, 0,$length) === $directory) {
+                    unset($this->_resource_dir[ $dir ]);
+                }
+            }
+        }
+        foreach ($newDir as $directory) {
+            $directory = $this->smarty->_realpath($directory, true);
+            $this->_resource_dir[ $directory ] = true;
+        }
+    }
     /**
      * Check if file is inside a valid directory
      *
      * @param string $filepath
      * @param array  $dirs valid directories
      *
-     * @return array
+     * @return array|bool
      * @throws \SmartyException
      */
     private function _checkDir($filepath, $dirs)
     {
+        $directory = dirname($filepath) . DIRECTORY_SEPARATOR;
+        if (isset($dirs[ $directory ])) {
+            return false;
+        }
+        $filepath = $this->smarty->_realpath($filepath, true);
         $directory = dirname($filepath) . DIRECTORY_SEPARATOR;
         $_directory = array();
         while (true) {
-            // remember the directory to add it to _resource_dir in case we're successful
-            $_directory[ $directory ] = true;
-            // test if the directory is trusted
+             // test if the directory is trusted
             if (isset($dirs[ $directory ])) {
-                // merge sub directories of current $directory into _resource_dir to speed up subsequent lookup
-                $dirs = array_merge($dirs, $_directory);
-
-                return $dirs;
+               return $_directory;
             }
             // abort if we've reached root
             if (!preg_match('#[\\\/][^\\\/]+[\\\/]$#', $directory)) {
                 break;
             }
-            // bubble up one level
+            // remember the directory to add it to _resource_dir in case we're successful
+            $_directory[ $directory ] = true;
+           // bubble up one level
             $directory = preg_replace('#[\\\/][^\\\/]+[\\\/]$#', DIRECTORY_SEPARATOR, $directory);
         }
 
         // give up
         throw new SmartyException("directory '{$filepath}' not allowed by security setting");

Vulnerable code:     const SMARTY_VERSION = '3.1.32-dev-45';
        $nds = DIRECTORY_SEPARATOR === '/' ? '\\' : '/';
        $path = str_replace($nds, DIRECTORY_SEPARATOR, $path);
        // remove noop 'DIRECTORY_SEPARATOR DIRECTORY_SEPARATOR' and 'DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR' patterns
        $path = preg_replace('#([\\\\/]([.]?[\\\\/])+)#u', DIRECTORY_SEPARATOR, $path);
            preg_match_all('#(([.]?[\\\\/])*([.][.])[\\\\/]([.]?[\\\\/])*)+#u', $path, $match)
                $path = preg_replace('#(([\\\\/]([.]?[\\\\/])*[^\\\\/.]+){' . $count .
                                     '}[\\\\/]([.]?[\\\\/])*([.][.][\\\\/]([.]?[\\\\/])*){' . $count . '})(?=[^.])#u',
        return $parts[ 'root' ] . $path;
            foreach ($this->_include_dir as $directory) {
                unset($this->_resource_dir[ $directory ]);
            }
            if ($this->smarty->use_include_path) {
                $this->_include_dir = array();
                $_dirs = $this->smarty->ext->_getIncludePath->getIncludePathDirs($this->smarty);
                foreach ($_dirs as $directory) {
                    $this->_include_dir[] = $directory;
                    $this->_resource_dir[ $directory ] = true;
                }
        if ($isConfig !== true &&
            (!isset($this->smarty->_cache[ 'template_dir_new' ]) || $this->smarty->_cache[ 'template_dir_new' ])
        ) {
                foreach ($this->_template_dir as $directory) {
                    unset($this->_resource_dir[ $directory ]);
                }
                foreach ($_dir as $directory) {
                    $this->_resource_dir[ $directory ] = true;
                }
            $this->smarty->_cache[ 'template_dir_new' ] = false;
        if ($isConfig !== false &&
            (!isset($this->smarty->_cache[ 'config_dir_new' ]) || $this->smarty->_cache[ 'config_dir_new' ])
        ) {
                foreach ($this->_config_dir as $directory) {
                    unset($this->_resource_dir[ $directory ]);
                }
                foreach ($_dir as $directory) {
                    $this->_resource_dir[ $directory ] = true;
                }
            $this->smarty->_cache[ 'config_dir_new' ] = false;
        if ($this->_secure_dir !== (array) $this->secure_dir) {
            foreach ($this->_secure_dir as $directory) {
                unset($this->_resource_dir[ $directory ]);
            foreach ((array) $this->secure_dir as $directory) {
                $directory = $this->smarty->_realpath($directory . DIRECTORY_SEPARATOR, true);
                $this->_resource_dir[ $directory ] = true;
            }
            $this->_secure_dir = (array) $this->secure_dir;
        $this->_resource_dir = $this->_checkDir($filepath, $this->_resource_dir);

        $this->_php_resource_dir =
            $this->_checkDir($this->smarty->_realpath($filepath, true), $this->_php_resource_dir);
        return true;
     * @return array
            // remember the directory to add it to _resource_dir in case we're successful
            $_directory[ $directory ] = true;
            // test if the directory is trusted
                // merge sub directories of current $directory into _resource_dir to speed up subsequent lookup
                $dirs = array_merge($dirs, $_directory);

                return $dirs;
            // bubble up one level
Fixed code: 24.04.2018
 - bugfix  possible Security Vulnerability in Smarty_Security class.

    const SMARTY_VERSION = '3.1.32-dev-46';
        static $nds = array('/' => '\\', '\\' => '/');
        static $sepDotsep = DIRECTORY_SEPARATOR . '.' . DIRECTORY_SEPARATOR;
        static $sepDot = DIRECTORY_SEPARATOR . '.';
        static $sepSep = DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR;
        $path = str_replace(array($nds[DIRECTORY_SEPARATOR], $sepDotsep), DIRECTORY_SEPARATOR, $path);
        if (strpos($path,$sepDot) === false && (($realpath === false && $path[0] === '.') || $realpath === null) && $path[0] !== '\\') {
            return $path;
        }
       // remove noop 'DIRECTORY_SEPARATOR DIRECTORY_SEPARATOR' and 'DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR' patterns
        $path = str_replace(array($sepDotsep,$sepSep), DIRECTORY_SEPARATOR, $path);
            preg_match_all('#[\\\\/]([.][.][\\\\/])+#u', $path, $match)
                $path = preg_replace('#([\\\\/]+[^\\\\/]+){' . $count .
                                     '}[\\\\/]+([.][.][\\\\/]+){' . $count . '}#u',
        return $realpath !== false ? $parts[ 'root' ] . $path : str_ireplace(getcwd(), '.', $parts[ 'root' ] . $path);
            $_dir = $this->smarty->use_include_path ? $this->smarty->ext->_getIncludePath->getIncludePathDirs($this->smarty) : array();
            if ($this->_include_dir !== $_dir) {
                $this->_updateResourceDir($this->_include_dir, $_dir);
                $this->_include_dir = $_dir;
        if ($isConfig !== true) {
                $this->_updateResourceDir($this->_template_dir, $_dir);
        if ($isConfig !== false) {
                $this->_updateResourceDir($this->_config_dir, $_dir);
        if ($this->_secure_dir !== $this->secure_dir) {
            $this->secure_dir = (array)$this->secure_dir;
            foreach($this->secure_dir as $k => $d) {
                $this->secure_dir[$k] = $this->smarty->_realpath($d.DIRECTORY_SEPARATOR,true);
            $this->_updateResourceDir($this->_secure_dir, $this->secure_dir);
            $this->_secure_dir = $this->secure_dir;
        }
        $addPath =  $this->_checkDir($filepath, $this->_resource_dir);
        if ($addPath !== false) {
           $this->_resource_dir = array_merge($this->_resource_dir, $addPath);
        $addPath =  $this->_checkDir($filepath, $this->_php_resource_dir);
        if ($addPath !== false) {
           $this->_php_resource_dir = array_merge($this->_php_resource_dir, $addPath);
        }
         return true;
    /**
     * Remove old directories and its sub folders, add new directories
     *
     * @param array $oldDir
     * @param array $newDir
     */
    private function _updateResourceDir($oldDir, $newDir) {
        foreach ($oldDir as $directory) {
            $directory = $this->smarty->_realpath($directory, true);
            $length = strlen($directory);
            foreach ($this->_resource_dir as $dir) {
                if (substr($dir, 0,$length) === $directory) {
                    unset($this->_resource_dir[ $dir ]);
                }
            }
        }
        foreach ($newDir as $directory) {
            $directory = $this->smarty->_realpath($directory, true);
            $this->_resource_dir[ $directory ] = true;
        }
    }
     * @return array|bool
        $directory = dirname($filepath) . DIRECTORY_SEPARATOR;
        if (isset($dirs[ $directory ])) {
            return false;
        }
        $filepath = $this->smarty->_realpath($filepath, true);
             // test if the directory is trusted
               return $_directory;
            // remember the directory to add it to _resource_dir in case we're successful
            $_directory[ $directory ] = true;
           // bubble up one level