CVE-2021-32682 (PHP, published 2021-06-14T17:15Z)
Description: 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.
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
Repository: https://github.com/Studio-42/elFinder
Files changed:
php/elFinder.class.php
php/elFinderVolumeDriver.class.php
php/elFinderVolumeLocalFileSystem.class.php
php/elFinderVolumeSFTPphpseclib.class.php
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()

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));