Skip to content
Permalink
Browse files Browse the repository at this point in the history
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()
  • Loading branch information
nao-pon committed Jun 13, 2021
1 parent 60d1d76 commit a106c35
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 77 deletions.
213 changes: 153 additions & 60 deletions php/elFinder.class.php
Expand Up @@ -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()
*
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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
*
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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'];

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -5279,4 +5372,4 @@ class elFinderAbortException extends Exception

class elFinderTriggerException extends Exception
{
}
}
20 changes: 13 additions & 7 deletions php/elFinderVolumeDriver.class.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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'));
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit a106c35

Please sign in to comment.