Headline
CVE-2021-32682: Merge pull request from GHSA-wph3-44rj-92pr · Studio-42/elFinder@a106c35
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.
@@ -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 { } }