CVE-2019-16790: Unrestricted Upload of File with Dangerous Type (CWE-434)
value: In Tiny File Manager before 2.3.9, there is a remote code execution via Upload from URL and Edit/Rename files. Only authenticated users are impacted.
CWE description: The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.
CVSS v3 base score: 8.8
Published: 2019-12-30T20:15Z
Repository: https://github.com/prasathmani/tinyfilemanager
Language: PHP
Files changed: tinyfilemanager.php
Commit date: 2019-12-28 19:23:47 +0530
Commit message: Merge pull request from GHSA-w72h-v37j-rrwr

* Fix the RCE vuln via Upload from URL

This commit attemps to fix the Remote Code Execution
(authenticated) via Upload from URL. Some notes about
the proposed solution:

* A new function (fm_is_file_allowed) has been created to
validate if the filename is allowed. This function gets the
the filename as parameter and returns true if it validates
as allowed. Otherwise returns false (the default).

* It's better to have such validatation(s) in one place
instead of spread all over the code. There are other places in
the application where the filename is validated and they should
all be refactored to call this function. Then we can focus
all needed validations in one place only!

NOTE: This refactoring was not done - the only goal was to fix
this security vulnerability only.

* The fm_is_file_allowed() function validates the filename
based on its extension only. No other validatation(s) have been
implemented in this commit.

* File extensions are assumed to be case-insensitive.
For example, php == PHP == Php == PhP, etc. This is consitent
with some web servers. Without this, the user will have to populate
the $allowed_extensions with all possible allowed combinations.

* Although, there is one drawback to the current solution, which
is that all files must have an extension to be uploaded. This is not
consitent with modern filesystems. Maybe a better solution would be
to automatically append an extension to the filename if no
extension has been found (e.g., .html or .txt which are generally
considered to be harmless). This must be decided by the
application's maintainers.

* Fix the RCE vulns via new/rename file

Sanitize the arguments to stat using escapeshellarg()

Co-authored-by: Jorge Morgado <jorge@morgado.ch>

Diff: diff --git a/tinyfilemanager.php b/tinyfilemanager.php
index f1becd3..94246b8 100644
--- a/tinyfilemanager.php
+++ b/tinyfilemanager.php
@@ -7,11 +7,11 @@ $CONFIG = '{"lang":"en","error_reporting":false,"show_hidden":false,"hide_Cols":
  * CCP Programmers | ccpprogrammers@gmail.com
  * https://tinyfilemanager.github.io
  */
 
 //TFM version
-define('VERSION', '2.3.8');
+define('VERSION', '2.3.9');
 
 //Application Title
 define('APP_TITLE', 'Tiny File Manager');
 
 // Auth with login/password (set true/false to enable/disable it)
@@ -478,10 +478,16 @@ if (isset($_POST['ajax']) && !FM_READONLY) {
             global $path, $fileinfo, $temp_file;
             return $path."/".basename($fileinfo->name);
         }
 
         $err = false;
+        if(!fm_is_file_allowed($fileinfo->name)) {
+            $err = array("message" => "File extension is not allowed");
+            event_callback(array("fail" => $err));
+            exit();
+        }
+
         if (!$url) {
             $success = false;
         } else if ($use_curl) {
             @$fp = fopen($temp_file, "w");
             @$ch = curl_init($url);
@@ -1918,10 +1924,31 @@ fm_show_footer();
 
 //--- END
 
 // Functions
 
+/**
+ * Check if the filename is allowed.
+ * @param string $filename
+ * @return bool
+ */
+function fm_is_file_allowed($filename)
+{
+    // By default, no file is allowed
+    $allowed = false;
+
+    if (FM_EXTENSION) {
+        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+
+        if (in_array($ext, explode(',', strtolower(FM_EXTENSION)))) {
+            $allowed = true;
+        }
+    }
+
+    return $allowed;
+}
+
 /**
  * Delete  file or folder (recursively)
  * @param string $path
  * @return bool
  */
@@ -2213,11 +2240,12 @@ function fm_get_size($file)
         $exec_works = (function_exists('exec') && !ini_get('safe_mode') && @exec('echo EXEC') == 'EXEC');
     }
 
     // try a shell command
     if ($exec_works) {
-        $cmd = ($iswin) ? "for %F in (\"$file\") do @echo %~zF" : ($isdarwin ? "stat -f%z \"$file\"" : "stat -c%s \"$file\"");
+        $arg = escapeshellarg($file);
+        $cmd = ($iswin) ? "for %F in (\"$file\") do @echo %~zF" : ($isdarwin ? "stat -f%z $arg" : "stat -c%s $arg");
         @exec($cmd, $output);
         if (is_array($output) && ctype_digit($size = trim(implode("\n", $output)))) {
             return $size;
         }
     }

Vulnerable code: define('VERSION', '2.3.8');
        $cmd = ($iswin) ? "for %F in (\"$file\") do @echo %~zF" : ($isdarwin ? "stat -f%z \"$file\"" : "stat -c%s \"$file\"");
Fixed code: define('VERSION', '2.3.9');
        if(!fm_is_file_allowed($fileinfo->name)) {
            $err = array("message" => "File extension is not allowed");
            event_callback(array("fail" => $err));
            exit();
        }

/**
 * Check if the filename is allowed.
 * @param string $filename
 * @return bool
 */
function fm_is_file_allowed($filename)
{
    // By default, no file is allowed
    $allowed = false;

    if (FM_EXTENSION) {
        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

        if (in_array($ext, explode(',', strtolower(FM_EXTENSION)))) {
            $allowed = true;
        }
    }

    return $allowed;
}

        $arg = escapeshellarg($file);
        $cmd = ($iswin) ? "for %F in (\"$file\") do @echo %~zF" : ($isdarwin ? "stat -f%z $arg" : "stat -c%s $arg");