Critical severityNVD Advisory· Published Jun 14, 2021· Updated Aug 3, 2024
Multiple vulnerabilities leading to RCE
CVE-2021-32682
Description
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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
studio-42/elfinderPackagist | < 2.1.59 | 2.1.59 |
Affected products
1Patches
1a106c350b7dfMerge pull request from GHSA-wph3-44rj-92pr
4 files changed · +195 −77
php/elFinder.class.php+153 −60 modified@@ -419,6 +419,21 @@ class elFinder */ 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() * @@ -713,6 +728,10 @@ public function __construct($opts) $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'; @@ -2524,16 +2543,87 @@ protected function abort($args = array()) } $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 * @@ -2552,54 +2642,20 @@ protected function abort($args = array()) 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; } @@ -2619,8 +2675,11 @@ protected function get_remote_contents(&$url, $timeout = 30, $redirect_max = 5, * @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); @@ -2633,11 +2692,19 @@ protected function curl_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp 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; } @@ -2658,7 +2725,7 @@ protected function curl_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp * @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; @@ -2669,22 +2736,15 @@ protected function fsock_get_contents(&$url, $timeout, $redirect_max, $ua, $outf $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']; @@ -2765,7 +2825,11 @@ protected function fsock_get_contents(&$url, $timeout, $redirect_max, $ua, $outf 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: @@ -3831,7 +3895,8 @@ 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); } @@ -4339,7 +4404,7 @@ 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); @@ -4368,7 +4433,7 @@ protected function itemLock($hashes, $autoUnlock = true) $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 { @@ -4519,6 +4584,16 @@ 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 * @@ -5104,6 +5179,24 @@ public static function expandMemoryForGD($imgInfos) } } + /** + * 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 * @@ -5279,4 +5372,4 @@ class elFinderAbortException extends Exception class elFinderTriggerException extends Exception { -} \ No newline at end of file +}
php/elFinderVolumeDriver.class.php+13 −7 modified@@ -5451,7 +5451,7 @@ 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; @@ -6727,7 +6727,7 @@ protected function getArchivers($use_cache = true) 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); @@ -6737,17 +6737,17 @@ protected function getArchivers($use_cache = true) 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')); @@ -6872,8 +6872,14 @@ protected function makeArchive($dir, $files, $name, $arc) $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);
php/elFinderVolumeLocalFileSystem.class.php+24 −5 modified@@ -75,7 +75,6 @@ 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' @@ -172,14 +171,20 @@ protected function configure() } } // 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(); @@ -223,6 +228,8 @@ protected function configure() unset($hiddens['quarantine']); } } + } else if ($_path = elFinder::getCommonTempPath()) { + $this->quarantine = $_path; } if (!$this->quarantine) { @@ -346,7 +353,20 @@ protected function _basename($path) **/ 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; } /** @@ -1439,4 +1459,3 @@ protected function localFileSystemSymlink($target, $link) return $res; } } // END class -
php/elFinderVolumeSFTPphpseclib.class.php+5 −5 modified@@ -581,7 +581,7 @@ protected function _fclose($fp, $path = '') **/ 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; } @@ -601,7 +601,7 @@ protected function _mkdir($path, $name) **/ 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) { @@ -630,7 +630,7 @@ 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(); @@ -661,7 +661,7 @@ protected function _copy($source, $targetDir, $name) */ 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; } @@ -706,7 +706,7 @@ protected function _rmdir($path) 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;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-wph3-44rj-92prghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-32682ghsaADVISORY
- packetstormsecurity.com/files/164173/elFinder-Archive-Command-Injection.htmlghsax_refsource_MISCWEB
- blog.sonarsource.com/elfinder-case-study-of-web-file-manager-vulnerabilitiesghsaWEB
- blog.sonarsource.com/elfinder-case-study-of-web-file-manager-vulnerabilities/mitrex_refsource_MISC
- github.com/Studio-42/elFinder/commit/a106c350b7dfe666a81d6b576816db9fe0899b17ghsax_refsource_MISCWEB
- github.com/Studio-42/elFinder/security/advisories/GHSA-qm58-cvvm-c5qrghsaWEB
- github.com/Studio-42/elFinder/security/advisories/GHSA-wph3-44rj-92prghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.