CVE-2021-32682: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: elFinder is an open-source file manager for web, written in JavaScript using jQuery UI. Several vulnerabilities affect elFinder 2.1.58. These vulnerabilities can allow an attacker to execute arbitrary code and commands on the server hosting the elFinder PHP connector, even with minimal configuration. The issues were patched in version 2.1.59. As a workaround, ensure the connector is not exposed without authentication.
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: 9.8
Published: 2021-06-14T17:15Z
Repository: https://github.com/Studio-42/elFinder
Language: PHP
Files changed: php/elFinder.class.php, php/elFinderVolumeDriver.class.php, php/elFinderVolumeLocalFileSystem.class.php, php/elFinderVolumeSFTPphpseclib.class.php
Commit date: 2021-06-13 23:38:02 +0900
Commit message: Merge pull request from GHSA-wph3-44rj-92pr

* [php] fix multiple vulnerabilities

* fix archiver args

* fix remote Code Execution of zip command

* re-fix remote Code Execution of zip command

* re-fix Improper hostname validation in upload and put

* re-fix Directory traversal in the actions mkfile and mkdir

* Add check targets in archive()
Diff: diff --git a/php/elFinder.class.php b/php/elFinder.class.php
index a0b756645..bcc034c6f 100644
--- a/php/elFinder.class.php
+++ b/php/elFinder.class.php
@@ -417,10 +417,25 @@ class elFinder
      *
      * @var array
      */
     protected $removeContentSaveIds = array();
 
+    /**
+     * LAN class allowed when uploading via URL
+     * 
+     * Array keys are 'local', 'private_a', 'private_b', 'private_c' and 'link'
+     * 
+     * local:     127.0.0.0/8
+     * private_a: 10.0.0.0/8
+     * private_b: 172.16.0.0/12
+     * private_c: 192.168.0.0/16
+     * link:      169.254.0.0/16
+     *
+     * @var        array
+     */
+    protected $uploadAllowedLanIpClasses = array();
+
     /**
      * Flag of throw Error on exec()
      *
      * @var boolean
      */
@@ -711,10 +726,14 @@ class elFinder
         $this->optionsNetVolumes = (isset($opts['optionsNetVolumes']) && is_array($opts['optionsNetVolumes'])) ? $opts['optionsNetVolumes'] : array();
         if (isset($opts['itemLockExpire'])) {
             $this->itemLockExpire = intval($opts['itemLockExpire']);
         }
 
+        if (!empty($opts['uploadAllowedLanIpClasses'])) {
+            $this->uploadAllowedLanIpClasses = array_flip($opts['uploadAllowedLanIpClasses']);
+        }
+
         // deprecated settings
         $this->netVolumesSessionKey = !empty($opts['netVolumesSessionKey']) ? $opts['netVolumesSessionKey'] : 'elFinderNetVolumes';
         self::$sessionCacheKey = !empty($opts['sessionCacheKey']) ? $opts['sessionCacheKey'] : 'elFinderCaches';
 
         // check session cache
@@ -2522,20 +2541,91 @@ class elFinder
         if (!elFinder::$connectionFlagsPath || $_SERVER['REQUEST_METHOD'] === 'HEAD') {
             return;
         }
         $flagFile = elFinder::$connectionFlagsPath . DIRECTORY_SEPARATOR . 'elfreq%s';
         if (!empty($args['makeFile'])) {
-            self::$abortCheckFile = sprintf($flagFile, $args['makeFile']);
+            self::$abortCheckFile = sprintf($flagFile, self::filenameDecontaminate($args['makeFile']));
             touch(self::$abortCheckFile);
             $GLOBALS['elFinderTempFiles'][self::$abortCheckFile] = true;
             return;
         }
 
-        $file = !empty($args['id']) ? sprintf($flagFile, $args['id']) : self::$abortCheckFile;
+        $file = !empty($args['id']) ? sprintf($flagFile, self::filenameDecontaminate($args['id'])) : self::$abortCheckFile;
         $file && is_file($file) && unlink($file);
     }
 
+    /**
+     * Validate an URL to prevent SSRF attacks.
+     *
+     * To prevent any risk of DNS rebinding, always use the IP address resolved by
+     * this method, as returned in the array entry `ip`.
+     *
+     * @param string $url
+     *
+     * @return false|array
+     */
+    protected function validate_address($url)
+    {
+        $info = parse_url($url);
+        $host = trim(strtolower($info['host']), '.');
+        // do not support IPv6 address
+        if (preg_match('/^\[.*\]$/', $host)) {
+            return false;
+        }
+        // do not support non dot host
+        if (strpos($host, '.') === false) {
+            return false;
+        }
+        // do not support URL-encoded host
+        if (strpos($host, '%') !== false) {
+            return false;
+        }
+        // disallow including "localhost" and "localdomain"
+        if (preg_match('/\b(?:localhost|localdomain)\b/', $host)) {
+            return false;
+        }
+        // check IPv4 local loopback, private network and link local
+        $ip = gethostbyname($host);
+        if (preg_match('/^0x[0-9a-f]+|[0-9]+(?:\.(?:0x[0-9a-f]+|[0-9]+)){1,3}$/', $ip, $m)) {
+            $long = (int)sprintf('%u', ip2long($ip));
+            if (!$long) {
+                return false;
+            }
+            $local = (int)sprintf('%u', ip2long('127.255.255.255')) >> 24;
+            $prv1  = (int)sprintf('%u', ip2long('10.255.255.255')) >> 24;
+            $prv2  = (int)sprintf('%u', ip2long('172.31.255.255')) >> 20;
+            $prv3  = (int)sprintf('%u', ip2long('192.168.255.255')) >> 16;
+            $link  = (int)sprintf('%u', ip2long('169.254.255.255')) >> 16;
+
+            if (!isset($this->uploadAllowedLanIpClasses['local']) && $long >> 24 === $local) {
+                return false;
+            }
+            if (!isset($this->uploadAllowedLanIpClasses['private_a']) && $long >> 24 === $prv1) {
+                return false;
+            }
+            if (!isset($this->uploadAllowedLanIpClasses['private_b']) && $long >> 20 === $prv2) {
+                return false;
+            }
+            if (!isset($this->uploadAllowedLanIpClasses['private_c']) && $long >> 16 === $prv3) {
+                return false;
+            }
+            if (!isset($this->uploadAllowedLanIpClasses['link']) && $long >> 16 === $link) {
+                return false;
+            }
+            $info['ip'] = long2ip($long);
+            if (!isset($info['port'])) {
+                $info['port'] = $info['scheme'] === 'https' ? 443 : 80;
+            }
+            if (!isset($info['path'])) {
+                $info['path'] = '/';
+            }
+            return $info;
+        } else {
+            return false;
+        }
+    }
+
     /**
      * Get remote contents
      *
      * @param  string   $url          target url
      * @param  int      $timeout      timeout (sec)
@@ -2550,58 +2640,24 @@ class elFinder
      * @author  Naoki Sawada
      **/
     protected function get_remote_contents(&$url, $timeout = 30, $redirect_max = 5, $ua = 'Mozilla/5.0', $fp = null)
     {
         if (preg_match('~^(?:ht|f)tps?://[-_.!\~*\'()a-z0-9;/?:\@&=+\$,%#\*\[\]]+~i', $url)) {
-            $info = parse_url($url);
-            $host = trim(strtolower($info['host']), '.');
-            // do not support IPv6 address
-            if (preg_match('/^\[.*\]$/', $host)) {
-                return false;
-            }
-            // do not support non dot host
-            if (strpos($host, '.') === false) {
-                return false;
-            }
-            // do not support URL-encoded host
-            if (strpos($host, '%') !== false) {
+            $info = $this->validate_address($url);
+            if ($info === false) {
                 return false;
             }
-            // disallow including "localhost" and "localdomain"
-            if (preg_match('/\b(?:localhost|localdomain)\b/', $host)) {
-                return false;
-            }
-            // wildcard DNS (e.g xip.io)
-            if (preg_match('/0x[0-9a-f]+|[0-9]+(?:\.(?:0x[0-9a-f]+|[0-9]+)){1,3}/', $host)) {
-                $host = gethostbyname($host);
-            }
-            // check IPv4 local loopback, private network and link local
-            if (preg_match('/^0x[0-9a-f]+|[0-9]+(?:\.(?:0x[0-9a-f]+|[0-9]+)){1,3}$/', $host, $m)) {
-                $long = (int)sprintf('%u', ip2long($host));
-                if (!$long) {
-                    return false;
-                }
-                $local = (int)sprintf('%u', ip2long('127.255.255.255')) >> 24;
-                $prv1 = (int)sprintf('%u', ip2long('10.255.255.255')) >> 24;
-                $prv2 = (int)sprintf('%u', ip2long('172.31.255.255')) >> 20;
-                $prv3 = (int)sprintf('%u', ip2long('192.168.255.255')) >> 16;
-                $link = (int)sprintf('%u', ip2long('169.254.255.255')) >> 16;
-
-                if ($long >> 24 === $local || $long >> 24 === $prv1 || $long >> 20 === $prv2 || $long >> 16 === $prv3 || $long >> 16 === $link) {
-                    return false;
-                }
-            }
             // dose not support 'user' and 'pass' for security reasons
-            $url = $info['scheme'].'://'.$host.(!empty($info['port'])? (':'.$info['port']) : '').$info['path'].(!empty($info['query'])? ('?'.$info['query']) : '').(!empty($info['fragment'])? ('#'.$info['fragment']) : '');
+            $url = $info['scheme'].'://'.$info['host'].(!empty($info['port'])? (':'.$info['port']) : '').$info['path'].(!empty($info['query'])? ('?'.$info['query']) : '').(!empty($info['fragment'])? ('#'.$info['fragment']) : '');
             // check by URL upload filter
             if ($this->urlUploadFilter && is_callable($this->urlUploadFilter)) {
                 if (!call_user_func_array($this->urlUploadFilter, array($url, $this))) {
                     return false;
                 }
             }
-            $method = (function_exists('curl_exec') && !ini_get('safe_mode') && !ini_get('open_basedir')) ? 'curl_get_contents' : 'fsock_get_contents';
-            return $this->$method($url, $timeout, $redirect_max, $ua, $fp);
+            $method = (function_exists('curl_exec')) ? 'curl_get_contents' : 'fsock_get_contents';
+            return $this->$method($url, $timeout, $redirect_max, $ua, $fp, $info);
         }
         return false;
     }
 
     /**
@@ -2617,12 +2673,15 @@ class elFinder
      * @retval string contents
      * @retval resource conttents
      * @retval false  error
      * @author Naoki Sawada
      **/
-    protected function curl_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp)
+    protected function curl_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp, $info)
     {
+        if ($redirect_max == 0) {
+            return false;
+        }
         $ch = curl_init();
         curl_setopt($ch, CURLOPT_URL, $url);
         curl_setopt($ch, CURLOPT_HEADER, false);
         if ($outfp) {
             curl_setopt($ch, CURLOPT_FILE, $outfp);
@@ -2631,15 +2690,23 @@ class elFinder
             curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
         }
         curl_setopt($ch, CURLOPT_LOW_SPEED_LIMIT, 1);
         curl_setopt($ch, CURLOPT_LOW_SPEED_TIME, $timeout);
         curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
-        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
-        curl_setopt($ch, CURLOPT_MAXREDIRS, $redirect_max);
+        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
         curl_setopt($ch, CURLOPT_USERAGENT, $ua);
+        curl_setopt($ch, CURLOPT_RESOLVE, [$info['host'] . ':' . $info['port'] . ':' . $info['ip']]);
         $result = curl_exec($ch);
-        $url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
+        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        if ($http_code == 301 || $http_code == 302) {
+            $new_url = curl_getinfo($ch, CURLINFO_REDIRECT_URL);
+            $info = $this->validate_address($new_url);
+            if ($info === false) {
+                return false;
+            }
+            return $this->curl_get_contents($new_url, $timeout, $redirect_max - 1, $ua, $outfp, $info);
+        }
         curl_close($ch);
         return $outfp ? $outfp : $result;
     }
 
     /**
@@ -2656,37 +2723,30 @@ class elFinder
      * @retval resource conttents
      * @retval false  error
      * @throws elFinderAbortException
      * @author Naoki Sawada
      */
-    protected function fsock_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp)
+    protected function fsock_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp, $info)
     {
         $connect_timeout = 3;
         $connect_try = 3;
         $method = 'GET';
         $readsize = 4096;
         $ssl = '';
 
         $getSize = null;
         $headers = '';
 
-        $arr = parse_url($url);
-        if (!$arr) {
-            // Bad request
-            return false;
-        }
+        $arr = $info;
         if ($arr['scheme'] === 'https') {
             $ssl = 'ssl://';
         }
 
         // query
         $arr['query'] = isset($arr['query']) ? '?' . $arr['query'] : '';
-        // port
-        $port = isset($arr['port']) ? $arr['port'] : '';
-        $arr['port'] = $port ? $port : ($ssl ? 443 : 80);
 
-        $url_base = $arr['scheme'] . '://' . $arr['host'] . ($port ? (':' . $port) : '');
+        $url_base = $arr['scheme'] . '://' . $info['host'] . ':' . $info['port'];
         $url_path = isset($arr['path']) ? $arr['path'] : '/';
         $uri = $url_path . $arr['query'];
 
         $query = $method . ' ' . $uri . " HTTP/1.0\r\n";
         $query .= "Host: " . $arr['host'] . "\r\n";
@@ -2763,11 +2823,15 @@ class elFinder
                     }
                     if ($_url === $url) {
                         sleep(1);
                     }
                     fclose($fp);
-                    return $this->fsock_get_contents($url, $timeout, $redirect_max, $ua, $outfp);
+                    $info = $this->validate_address($url);
+                    if ($info === false) {
+                        return false;
+                    }
+                    return $this->fsock_get_contents($url, $timeout, $redirect_max, $ua, $outfp, $info);
                 }
                 break;
             case 200:
                 $ret = true;
         }
@@ -3829,11 +3893,12 @@ class elFinder
     protected function archive($args)
     {
         $targets = isset($args['targets']) && is_array($args['targets']) ? $args['targets'] : array();
         $name = isset($args['name']) ? $args['name'] : '';
 
-        if (($volume = $this->volume($targets[0])) == false) {
+        $targets = array_filter($targets, array($this, 'volume'));
+        if (!$targets || ($volume = $this->volume($targets[0])) === false) {
             return $this->error(self::ERROR_ARCHIVE, self::ERROR_TRGDIR_NOT_FOUND);
         }
 
         foreach ($targets as $target) {
             $this->itemLock($target);
@@ -4337,11 +4402,11 @@ var go = function() {
     protected function itemLocked($hash)
     {
         if (!elFinder::$commonTempPath) {
             return false;
         }
-        $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . $hash . '.lock';
+        $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . self::filenameDecontaminate($hash) . '.lock';
         if (file_exists($lock)) {
             if (filemtime($lock) + $this->itemLockExpire < time()) {
                 unlink($lock);
                 return false;
             }
@@ -4366,11 +4431,11 @@ var go = function() {
         }
         if (!is_array($hashes)) {
             $hashes = array($hashes);
         }
         foreach ($hashes as $hash) {
-            $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . $hash . '.lock';
+            $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . self::filenameDecontaminate($hash) . '.lock';
             if ($this->itemLocked($hash)) {
                 $cnt = file_get_contents($lock) + 1;
             } else {
                 $cnt = 1;
             }
@@ -4517,10 +4582,20 @@ var go = function() {
     public static function getApiFullVersion()
     {
         return (string)self::$ApiVersion . '.' . (string)self::$ApiRevision;
     }
 
+    /**
+     * Return self::$commonTempPath
+     *
+     * @return     string  The common temporary path.
+     */
+    public static function getCommonTempPath()
+    {
+        return self::$commonTempPath;
+    }
+
     /**
      * Return Is Animation Gif
      *
      * @param  string $path server local path of target image
      *
@@ -5102,10 +5177,28 @@ var go = function() {
                 ini_set('memory_limit', $needs);
             }
         }
     }
 
+    /**
+     * Decontaminate of filename
+     *
+     * @param      String  $name   The name
+     *
+     * @return     String  Decontaminated filename
+     */
+    public static function filenameDecontaminate($name)
+    {
+        // Directory traversal defense
+        if (DIRECTORY_SEPARATOR === '\\') {
+            $name = str_replace('\\', '/', $name);
+        }
+        $parts = explode('/', trim($name, '/'));
+        $name = array_pop($parts); 
+        return $name;
+    }
+
     /**
      * Execute shell command
      *
      * @param  string $command      command line
      * @param  string $output       stdout strings
@@ -5277,6 +5370,6 @@ class elFinderAbortException extends Exception
 {
 }
 
 class elFinderTriggerException extends Exception
 {
-}
\ No newline at end of file
+}
diff --git a/php/elFinderVolumeDriver.class.php b/php/elFinderVolumeDriver.class.php
index 57d8cfb2b..0557bcab1 100644
--- a/php/elFinderVolumeDriver.class.php
+++ b/php/elFinderVolumeDriver.class.php
@@ -5449,11 +5449,11 @@ abstract class elFinderVolumeDriver
     protected function remove($path, $force = false)
     {
         $stat = $this->stat($path);
 
         if (empty($stat)) {
-            return $this->setError(elFinder::ERROR_RM, $path, elFinder::ERROR_FILE_NOT_FOUND);
+            return $this->setError(elFinder::ERROR_RM, $this->relpathCE($path), elFinder::ERROR_FILE_NOT_FOUND);
         }
 
         $stat['realpath'] = $path;
         $this->rmTmb($stat);
         $this->clearcache();
@@ -6725,31 +6725,31 @@ abstract class elFinderVolumeDriver
                 $arcs['extract']['application/zip'] = array('cmd' => ELFINDER_UNZIP_PATH, 'argc' => '-q', 'ext' => 'zip', 'toSpec' => '-d ', 'getsize' => array('argc' => '-Z -t', 'regex' => '/^.+?,\s?([0-9]+).+$/', 'replace' => '$1'));
             }
             unset($o);
             $this->procExec(ELFINDER_RAR_PATH, $o, $c);
             if ($c == 0 || $c == 7) {
-                $arcs['create']['application/x-rar'] = array('cmd' => ELFINDER_RAR_PATH, 'argc' => 'a -inul' . (defined('ELFINDER_RAR_MA4') && ELFINDER_RAR_MA4? ' -ma4' : ''), 'ext' => 'rar');
+                $arcs['create']['application/x-rar'] = array('cmd' => ELFINDER_RAR_PATH, 'argc' => 'a -inul' . (defined('ELFINDER_RAR_MA4') && ELFINDER_RAR_MA4? ' -ma4' : '') . ' --', 'ext' => 'rar');
             }
             unset($o);
             $this->procExec(ELFINDER_UNRAR_PATH, $o, $c);
             if ($c == 0 || $c == 7) {
                 $arcs['extract']['application/x-rar'] = array('cmd' => ELFINDER_UNRAR_PATH, 'argc' => 'x -y', 'ext' => 'rar', 'toSpec' => '', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)(?:(?:[^\r\n0-9]+[0-9]+[^\r\n0-9]+([0-9]+)[^\r\n]+)|(?:[^\r\n0-9]+([0-9]+)[^\r\n0-9]+[0-9]+[^\r\n]*))$/s', 'replace' => '$1'));
             }
             unset($o);
             $this->procExec(ELFINDER_7Z_PATH, $o, $c);
             if ($c == 0) {
-                $arcs['create']['application/x-7z-compressed'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a', 'ext' => '7z');
+                $arcs['create']['application/x-7z-compressed'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a --', 'ext' => '7z');
                 $arcs['extract']['application/x-7z-compressed'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'x -y', 'ext' => '7z', 'toSpec' => '-o', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]+([0-9]+)[^\r\n]+$/s', 'replace' => '$1'));
 
                 if (empty($arcs['create']['application/zip'])) {
-                    $arcs['create']['application/zip'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -tzip', 'ext' => 'zip');
+                    $arcs['create']['application/zip'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -tzip --', 'ext' => 'zip');
                 }
                 if (empty($arcs['extract']['application/zip'])) {
                     $arcs['extract']['application/zip'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'x -tzip -y', 'ext' => 'zip', 'toSpec' => '-o', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]+([0-9]+)[^\r\n]+$/s', 'replace' => '$1'));
                 }
                 if (empty($arcs['create']['application/x-tar'])) {
-                    $arcs['create']['application/x-tar'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -ttar', 'ext' => 'tar');
+                    $arcs['create']['application/x-tar'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -ttar --', 'ext' => 'tar');
                 }
                 if (empty($arcs['extract']['application/x-tar'])) {
                     $arcs['extract']['application/x-tar'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'x -ttar -y', 'ext' => 'tar', 'toSpec' => '-o', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]+([0-9]+)[^\r\n]+$/s', 'replace' => '$1'));
                 }
                 if (substr(PHP_OS, 0, 3) === 'WIN' && empty($arcs['extract']['application/x-rar'])) {
@@ -6870,12 +6870,18 @@ abstract class elFinderVolumeDriver
             if (chdir($dir)) {
                 foreach ($files as $i => $file) {
                     $files[$i] = '.' . DIRECTORY_SEPARATOR . basename($file);
                 }
                 $files = array_map('escapeshellarg', $files);
-
-                $cmd = $arc['cmd'] . ' ' . $arc['argc'] . ' ' . escapeshellarg($name) . ' ' . implode(' ', $files);
+                $prefix = $switch = '';
+                // The zip command accepts the "-" at the beginning of the file name as a command switch,
+                // and can't use '--' before archive name, so add "./" to name for security reasons.
+                if ($arc['ext'] === 'zip' && strpos($arc['argc'], '-tzip') === false) {
+                    $prefix = './';
+                    $switch = '-- ';
+                }
+                $cmd = $arc['cmd'] . ' ' . $arc['argc'] . ' ' . $prefix . escapeshellarg($name) . ' ' . $switch . implode(' ', $files);
                 $err_out = '';
                 $this->procExec($cmd, $o, $c, $err_out, $dir);
                 chdir($cwd);
             } else {
                 return false;
diff --git a/php/elFinderVolumeLocalFileSystem.class.php b/php/elFinderVolumeLocalFileSystem.class.php
index cc252db47..b3bb55179 100644
--- a/php/elFinderVolumeLocalFileSystem.class.php
+++ b/php/elFinderVolumeLocalFileSystem.class.php
@@ -73,11 +73,10 @@ class elFinderVolumeLocalFileSystem extends elFinderVolumeDriver
     public function __construct()
     {
         $this->options['alias'] = '';              // alias to replace root dir name
         $this->options['dirMode'] = 0755;            // new dirs mode
         $this->options['fileMode'] = 0644;            // new files mode
-        $this->options['quarantine'] = '.quarantine'; // quarantine folder name - required to check archive (must be hidden)
         $this->options['rootCssClass'] = 'elfinder-navbar-root-local';
         $this->options['followSymLinks'] = true;
         $this->options['detectDirIcon'] = '';         // file name that is detected as a folder icon e.g. '.diricon.png'
         $this->options['keepTimestamp'] = array('copy', 'move'); // keep timestamp at inner filesystem allowed 'copy', 'move' and 'upload'
         $this->options['substituteImg'] = true;       // support substitute image with dim command
@@ -170,18 +169,24 @@ class elFinderVolumeLocalFileSystem extends elFinderVolumeDriver
             } else {
                 $this->options['tmpPath'] = $this->_normpath($this->options['tmpPath']);
             }
         }
         // check quarantine path
+        $_quarantine = '';
         if (!empty($this->options['quarantine'])) {
             if (strpos($this->options['quarantine'], DIRECTORY_SEPARATOR) === false) {
-                $hiddens['quarantine'] = $this->options['quarantine'];
-                $this->options['quarantine'] = $this->_abspath($this->options['quarantine']);
+                //$hiddens['quarantine'] = $this->options['quarantine'];
+                //$this->options['quarantine'] = $this->_abspath($this->options['quarantine']);
+                $_quarantine = $this->_abspath($this->options['quarantine']);
+                $this->options['quarantine'] = '';
             } else {
                 $this->options['quarantine'] = $this->_normpath($this->options['quarantine']);
             }
+        } else {
+            $_quarantine = $this->_abspath('.quarantine');
         }
+        is_dir($_quarantine) && self::localRmdirRecursive($_quarantine);
 
         parent::configure();
 
         // check tmbPath
         if (!$this->tmbPath && isset($hiddens['tmb'])) {
@@ -221,10 +226,12 @@ class elFinderVolumeLocalFileSystem extends elFinderVolumeDriver
             } else {
                 if (isset($hiddens['quarantine'])) {
                     unset($hiddens['quarantine']);
                 }
             }
+        } else if ($_path = elFinder::getCommonTempPath()) {
+            $this->quarantine = $_path;
         }
 
         if (!$this->quarantine) {
             if (!$this->tmp) {
                 $this->archivers['extract'] = array();
@@ -344,11 +351,24 @@ class elFinderVolumeLocalFileSystem extends elFinderVolumeDriver
      * @return string
      * @author Dmitry (dio) Levashov
      **/
     protected function _joinPath($dir, $name)
     {
-        return rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
+        $dir = rtrim($dir, DIRECTORY_SEPARATOR);
+        $path = realpath($dir . DIRECTORY_SEPARATOR . $name);
+        // realpath() returns FALSE if the file does not exist
+        if ($path === false || strpos($path, $this->root) !== 0) {
+            if (DIRECTORY_SEPARATOR !== '/') {
+                $name = str_replace('/', DIRECTORY_SEPARATOR, $name);
+            }
+            // Directory traversal measures
+            if (strpos($name, '..' . DIRECTORY_SEPARATOR) !== false) {
+                $name = basename($name);
+            }
+            $path = $dir . DIRECTORY_SEPARATOR . $name;
+        }
+        return $path; 
     }
 
     /**
      * Return normalized path, this works the same as os.path.normpath() in Python
      *
@@ -1437,6 +1457,5 @@ class elFinderVolumeLocalFileSystem extends elFinderVolumeDriver
         }
         error_reporting($errlev);
         return $res;
     }
 } // END class 
-
diff --git a/php/elFinderVolumeSFTPphpseclib.class.php b/php/elFinderVolumeSFTPphpseclib.class.php
index 9f0bf6bff..f35ccfd3f 100644
--- a/php/elFinderVolumeSFTPphpseclib.class.php
+++ b/php/elFinderVolumeSFTPphpseclib.class.php
@@ -579,11 +579,11 @@ class elFinderVolumeSFTPphpseclib extends elFinderVolumeFTP {
      * @return string|bool
      * @author Dmitry (dio) Levashov
      **/
     protected function _mkdir($path, $name)
     {
-        $path = $this->_joinPath($path, $name);
+        $path = $this->_joinPath($path, $this->_basename($name));
         if ($this->connect->mkdir($path) === false) {
             return false;
         }
 
         $this->options['dirMode'] && $this->connect->chmod($this->options['dirMode'], $path);
@@ -599,11 +599,11 @@ class elFinderVolumeSFTPphpseclib extends elFinderVolumeFTP {
      * @return string|bool
      * @author sitecode
      **/
     protected function _mkfile($path, $name)
     {
-        $path = $this->_joinPath($path, $name);
+        $path = $this->_joinPath($path, $this->_basename($name));
         return $this->connect->put($path, '') ? $path : false;
 /*
         if ($this->tmp) {
             $path = $this->_joinPath($path, $name);
             $local = $this->getTempFile();
@@ -628,11 +628,11 @@ class elFinderVolumeSFTPphpseclib extends elFinderVolumeFTP {
      **/
     protected function _copy($source, $targetDir, $name)
     {
         $res = false;
 
-        $target = $this->_joinPath($targetDir, $name);
+        $target = $this->_joinPath($targetDir, $this->_basename($name));
         if ($this->tmp) {
             $local = $this->getTempFile();
 
             if ($this->connect->get($source, $local)
                 && $this->connect->put($target, $local, NET_SFTP_LOCAL_FILE)) {
@@ -659,11 +659,11 @@ class elFinderVolumeSFTPphpseclib extends elFinderVolumeFTP {
      * @internal param string $target target dir path
      * @author   Dmitry (dio) Levashov
      */
     protected function _move($source, $targetDir, $name)
     {
-        $target = $this->_joinPath($targetDir, $name);
+        $target = $this->_joinPath($targetDir, $this->_basename($name));
         return $this->connect->rename($source, $target) ? $target : false;
     }
 
     /**
      * Remove file
@@ -704,11 +704,11 @@ class elFinderVolumeSFTPphpseclib extends elFinderVolumeFTP {
      * @author Dmitry (dio) Levashov
      **/
     protected function _save($fp, $dir, $name, $stat)
     {
         //TODO optionally encrypt $fp before uploading if mime is not already encrypted type
-        $path = $this->_joinPath($dir, $name);
+        $path = $this->_joinPath($dir, $this->_basename($name));
         return $this->connect->put($path, $fp)
             ? $path
             : false;
     }
 

Vulnerable code:             self::$abortCheckFile = sprintf($flagFile, $args['makeFile']);
        $file = !empty($args['id']) ? sprintf($flagFile, $args['id']) : self::$abortCheckFile;
            $info = parse_url($url);
            $host = trim(strtolower($info['host']), '.');
            // do not support IPv6 address
            if (preg_match('/^\[.*\]$/', $host)) {
                return false;
            }
            // do not support non dot host
            if (strpos($host, '.') === false) {
                return false;
            }
            // do not support URL-encoded host
            if (strpos($host, '%') !== false) {
            // disallow including "localhost" and "localdomain"
            if (preg_match('/\b(?:localhost|localdomain)\b/', $host)) {
                return false;
            }
            // wildcard DNS (e.g xip.io)
            if (preg_match('/0x[0-9a-f]+|[0-9]+(?:\.(?:0x[0-9a-f]+|[0-9]+)){1,3}/', $host)) {
                $host = gethostbyname($host);
            }
            // check IPv4 local loopback, private network and link local
            if (preg_match('/^0x[0-9a-f]+|[0-9]+(?:\.(?:0x[0-9a-f]+|[0-9]+)){1,3}$/', $host, $m)) {
                $long = (int)sprintf('%u', ip2long($host));
                if (!$long) {
                    return false;
                }
                $local = (int)sprintf('%u', ip2long('127.255.255.255')) >> 24;
                $prv1 = (int)sprintf('%u', ip2long('10.255.255.255')) >> 24;
                $prv2 = (int)sprintf('%u', ip2long('172.31.255.255')) >> 20;
                $prv3 = (int)sprintf('%u', ip2long('192.168.255.255')) >> 16;
                $link = (int)sprintf('%u', ip2long('169.254.255.255')) >> 16;

                if ($long >> 24 === $local || $long >> 24 === $prv1 || $long >> 20 === $prv2 || $long >> 16 === $prv3 || $long >> 16 === $link) {
                    return false;
                }
            }
            $url = $info['scheme'].'://'.$host.(!empty($info['port'])? (':'.$info['port']) : '').$info['path'].(!empty($info['query'])? ('?'.$info['query']) : '').(!empty($info['fragment'])? ('#'.$info['fragment']) : '');
            $method = (function_exists('curl_exec') && !ini_get('safe_mode') && !ini_get('open_basedir')) ? 'curl_get_contents' : 'fsock_get_contents';
            return $this->$method($url, $timeout, $redirect_max, $ua, $fp);
    protected function curl_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp)
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_MAXREDIRS, $redirect_max);
        $url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
    protected function fsock_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp)
        $arr = parse_url($url);
        if (!$arr) {
            // Bad request
            return false;
        }
        // port
        $port = isset($arr['port']) ? $arr['port'] : '';
        $arr['port'] = $port ? $port : ($ssl ? 443 : 80);
        $url_base = $arr['scheme'] . '://' . $arr['host'] . ($port ? (':' . $port) : '');
                    return $this->fsock_get_contents($url, $timeout, $redirect_max, $ua, $outfp);
        if (($volume = $this->volume($targets[0])) == false) {
        $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . $hash . '.lock';
            $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . $hash . '.lock';
}
            return $this->setError(elFinder::ERROR_RM, $path, elFinder::ERROR_FILE_NOT_FOUND);
                $arcs['create']['application/x-rar'] = array('cmd' => ELFINDER_RAR_PATH, 'argc' => 'a -inul' . (defined('ELFINDER_RAR_MA4') && ELFINDER_RAR_MA4? ' -ma4' : ''), 'ext' => 'rar');
                $arcs['create']['application/x-7z-compressed'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a', 'ext' => '7z');
                    $arcs['create']['application/zip'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -tzip', 'ext' => 'zip');
                    $arcs['create']['application/x-tar'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -ttar', 'ext' => 'tar');

                $cmd = $arc['cmd'] . ' ' . $arc['argc'] . ' ' . escapeshellarg($name) . ' ' . implode(' ', $files);
        $this->options['quarantine'] = '.quarantine'; // quarantine folder name - required to check archive (must be hidden)
                $hiddens['quarantine'] = $this->options['quarantine'];
                $this->options['quarantine'] = $this->_abspath($this->options['quarantine']);
        return rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;

        $path = $this->_joinPath($path, $name);
        $path = $this->_joinPath($path, $name);
        $target = $this->_joinPath($targetDir, $name);
        $target = $this->_joinPath($targetDir, $name);
        $path = $this->_joinPath($dir, $name);
Fixed code:     /**
     * LAN class allowed when uploading via URL
     * 
     * Array keys are 'local', 'private_a', 'private_b', 'private_c' and 'link'
     * 
     * local:     127.0.0.0/8
     * private_a: 10.0.0.0/8
     * private_b: 172.16.0.0/12
     * private_c: 192.168.0.0/16
     * link:      169.254.0.0/16
     *
     * @var        array
     */
    protected $uploadAllowedLanIpClasses = array();

        if (!empty($opts['uploadAllowedLanIpClasses'])) {
            $this->uploadAllowedLanIpClasses = array_flip($opts['uploadAllowedLanIpClasses']);
        }

            self::$abortCheckFile = sprintf($flagFile, self::filenameDecontaminate($args['makeFile']));
        $file = !empty($args['id']) ? sprintf($flagFile, self::filenameDecontaminate($args['id'])) : self::$abortCheckFile;
    /**
     * Validate an URL to prevent SSRF attacks.
     *
     * To prevent any risk of DNS rebinding, always use the IP address resolved by
     * this method, as returned in the array entry `ip`.
     *
     * @param string $url
     *
     * @return false|array
     */
    protected function validate_address($url)
    {
        $info = parse_url($url);
        $host = trim(strtolower($info['host']), '.');
        // do not support IPv6 address
        if (preg_match('/^\[.*\]$/', $host)) {
            return false;
        }
        // do not support non dot host
        if (strpos($host, '.') === false) {
            return false;
        }
        // do not support URL-encoded host
        if (strpos($host, '%') !== false) {
            return false;
        }
        // disallow including "localhost" and "localdomain"
        if (preg_match('/\b(?:localhost|localdomain)\b/', $host)) {
            return false;
        }
        // check IPv4 local loopback, private network and link local
        $ip = gethostbyname($host);
        if (preg_match('/^0x[0-9a-f]+|[0-9]+(?:\.(?:0x[0-9a-f]+|[0-9]+)){1,3}$/', $ip, $m)) {
            $long = (int)sprintf('%u', ip2long($ip));
            if (!$long) {
                return false;
            }
            $local = (int)sprintf('%u', ip2long('127.255.255.255')) >> 24;
            $prv1  = (int)sprintf('%u', ip2long('10.255.255.255')) >> 24;
            $prv2  = (int)sprintf('%u', ip2long('172.31.255.255')) >> 20;
            $prv3  = (int)sprintf('%u', ip2long('192.168.255.255')) >> 16;
            $link  = (int)sprintf('%u', ip2long('169.254.255.255')) >> 16;

            if (!isset($this->uploadAllowedLanIpClasses['local']) && $long >> 24 === $local) {
                return false;
            }
            if (!isset($this->uploadAllowedLanIpClasses['private_a']) && $long >> 24 === $prv1) {
                return false;
            }
            if (!isset($this->uploadAllowedLanIpClasses['private_b']) && $long >> 20 === $prv2) {
                return false;
            }
            if (!isset($this->uploadAllowedLanIpClasses['private_c']) && $long >> 16 === $prv3) {
                return false;
            }
            if (!isset($this->uploadAllowedLanIpClasses['link']) && $long >> 16 === $link) {
                return false;
            }
            $info['ip'] = long2ip($long);
            if (!isset($info['port'])) {
                $info['port'] = $info['scheme'] === 'https' ? 443 : 80;
            }
            if (!isset($info['path'])) {
                $info['path'] = '/';
            }
            return $info;
        } else {
            return false;
        }
    }

            $info = $this->validate_address($url);
            if ($info === false) {
            $url = $info['scheme'].'://'.$info['host'].(!empty($info['port'])? (':'.$info['port']) : '').$info['path'].(!empty($info['query'])? ('?'.$info['query']) : '').(!empty($info['fragment'])? ('#'.$info['fragment']) : '');
            $method = (function_exists('curl_exec')) ? 'curl_get_contents' : 'fsock_get_contents';
            return $this->$method($url, $timeout, $redirect_max, $ua, $fp, $info);
    protected function curl_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp, $info)
        if ($redirect_max == 0) {
            return false;
        }
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
        curl_setopt($ch, CURLOPT_RESOLVE, [$info['host'] . ':' . $info['port'] . ':' . $info['ip']]);
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($http_code == 301 || $http_code == 302) {
            $new_url = curl_getinfo($ch, CURLINFO_REDIRECT_URL);
            $info = $this->validate_address($new_url);
            if ($info === false) {
                return false;
            }
            return $this->curl_get_contents($new_url, $timeout, $redirect_max - 1, $ua, $outfp, $info);
        }
    protected function fsock_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp, $info)
        $arr = $info;
        $url_base = $arr['scheme'] . '://' . $info['host'] . ':' . $info['port'];
                    $info = $this->validate_address($url);
                    if ($info === false) {
                        return false;
                    }
                    return $this->fsock_get_contents($url, $timeout, $redirect_max, $ua, $outfp, $info);
        $targets = array_filter($targets, array($this, 'volume'));
        if (!$targets || ($volume = $this->volume($targets[0])) === false) {
        $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . self::filenameDecontaminate($hash) . '.lock';
            $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . self::filenameDecontaminate($hash) . '.lock';
    /**
     * Return self::$commonTempPath
     *
     * @return     string  The common temporary path.
     */
    public static function getCommonTempPath()
    {
        return self::$commonTempPath;
    }

    /**
     * Decontaminate of filename
     *
     * @param      String  $name   The name
     *
     * @return     String  Decontaminated filename
     */
    public static function filenameDecontaminate($name)
    {
        // Directory traversal defense
        if (DIRECTORY_SEPARATOR === '\\') {
            $name = str_replace('\\', '/', $name);
        }
        $parts = explode('/', trim($name, '/'));
        $name = array_pop($parts); 
        return $name;
    }

}
            return $this->setError(elFinder::ERROR_RM, $this->relpathCE($path), elFinder::ERROR_FILE_NOT_FOUND);
                $arcs['create']['application/x-rar'] = array('cmd' => ELFINDER_RAR_PATH, 'argc' => 'a -inul' . (defined('ELFINDER_RAR_MA4') && ELFINDER_RAR_MA4? ' -ma4' : '') . ' --', 'ext' => 'rar');
                $arcs['create']['application/x-7z-compressed'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a --', 'ext' => '7z');
                    $arcs['create']['application/zip'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -tzip --', 'ext' => 'zip');
                    $arcs['create']['application/x-tar'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -ttar --', 'ext' => 'tar');
                $prefix = $switch = '';
                // The zip command accepts the "-" at the beginning of the file name as a command switch,
                // and can't use '--' before archive name, so add "./" to name for security reasons.
                if ($arc['ext'] === 'zip' && strpos($arc['argc'], '-tzip') === false) {
                    $prefix = './';
                    $switch = '-- ';
                }
                $cmd = $arc['cmd'] . ' ' . $arc['argc'] . ' ' . $prefix . escapeshellarg($name) . ' ' . $switch . implode(' ', $files);
        $_quarantine = '';
                //$hiddens['quarantine'] = $this->options['quarantine'];
                //$this->options['quarantine'] = $this->_abspath($this->options['quarantine']);
                $_quarantine = $this->_abspath($this->options['quarantine']);
                $this->options['quarantine'] = '';
        } else {
            $_quarantine = $this->_abspath('.quarantine');
        is_dir($_quarantine) && self::localRmdirRecursive($_quarantine);
        } else if ($_path = elFinder::getCommonTempPath()) {
            $this->quarantine = $_path;
        $dir = rtrim($dir, DIRECTORY_SEPARATOR);
        $path = realpath($dir . DIRECTORY_SEPARATOR . $name);
        // realpath() returns FALSE if the file does not exist
        if ($path === false || strpos($path, $this->root) !== 0) {
            if (DIRECTORY_SEPARATOR !== '/') {
                $name = str_replace('/', DIRECTORY_SEPARATOR, $name);
            }
            // Directory traversal measures
            if (strpos($name, '..' . DIRECTORY_SEPARATOR) !== false) {
                $name = basename($name);
            }
            $path = $dir . DIRECTORY_SEPARATOR . $name;
        }
        return $path; 
        $path = $this->_joinPath($path, $this->_basename($name));
        $path = $this->_joinPath($path, $this->_basename($name));
        $target = $this->_joinPath($targetDir, $this->_basename($name));
        $target = $this->_joinPath($targetDir, $this->_basename($name));
        $path = $this->_joinPath($dir, $this->_basename($name));