CVE-2019-6257: Server-Side Request Forgery (SSRF) (CWE-918)
value: A Server Side Request Forgery (SSRF) vulnerability in elFinder before 2.1.46 could allow a malicious user to access the content of internal network resources. This occurs in get_remote_contents() in php/elFinder.class.php.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 7.7
Published: 2019-01-14T08:29Z
Repository: https://github.com/Studio-42/elFinder
Language: PHP
Files changed: php/elFinder.class.php
Commit date: 2019-01-13 18:51:30 +0900
Commit message: [php:core:security] fix SSRF vulnerability of `get_remote_contents()`

We express special gratitude to Do Ha Anh of Viettel Cyber Security
Center, the reporter of this vulnerability.

Diff: diff --git a/php/elFinder.class.php b/php/elFinder.class.php
index 38b1a9dd3..8ab213686 100644
--- a/php/elFinder.class.php
+++ b/php/elFinder.class.php
@@ -2408,27 +2408,45 @@ class elFinder
      **/
     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 = strtolower($info['host']);
+            $host = trim(strtolower($info['host']), '.');
             // do not support IPv6 address
             if (preg_match('/^\[.*\]$/', $host)) {
                 return false;
             }
-            // do not support non dot URL
+            // do not support non dot host
             if (strpos($host, '.') === false) {
                 return false;
             }
-            // disallow including "localhost"
-            if (strpos($host, 'localhost') !== false) {
+            // do not support URL-encoded host
+            if (strpos($host, '%') !== false) {
                 return false;
             }
-            // check IPv4 local loopback
-            if (preg_match('/^(?:127|0177|0x7f)\.[0-9a-fx.]+$/', $host)) {
+            // disallow including "localhost" and "localdomain"
+            if (preg_match('/\b(?:localhost|localdomain)\b/', $host)) {
                 return false;
             }
+            // 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']) : '');
             // check by URL upload filter
             if ($this->urlUploadFilter && is_callable($this->urlUploadFilter)) {
                 if (!call_user_func_array($this->urlUploadFilter, array($url, $this))) {
                     return false;
                 }
@@ -2549,10 +2567,14 @@ class elFinder
                 throw new elFinderAbortException();
             }
             sleep(1); // wait 1sec
         }
 
+        if (!$fp) {
+            return false;
+        }
+
         $fwrite = 0;
         for ($written = 0; $written < strlen($query); $written += $fwrite) {
             $fwrite = fwrite($fp, substr($query, $written));
             if (!$fwrite) {
                 break;

Vulnerable code:             $host = strtolower($info['host']);
            // do not support non dot URL
            // disallow including "localhost"
            if (strpos($host, 'localhost') !== false) {
            // check IPv4 local loopback
            if (preg_match('/^(?:127|0177|0x7f)\.[0-9a-fx.]+$/', $host)) {
Fixed code:             $host = trim(strtolower($info['host']), '.');
            // do not support non dot host
            // do not support URL-encoded host
            if (strpos($host, '%') !== false) {
            // disallow including "localhost" and "localdomain"
            if (preg_match('/\b(?:localhost|localdomain)\b/', $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']) : '');
        if (!$fp) {
            return false;
        }