CVE-2020-12103 (published 2020-04-28T22:15Z)

In Tiny File Manager 2.4.1 there is a vulnerability in the ajax file backup copy functionality which allows authenticated users to create backup copies of files (with .bak extension) outside the scope in the same directory in which they are stored.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:P/A:N)
CVSS v3 base score: 7.7, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/prasathmani/tinyfilemanager
commit_details: repo_url: https://github.com/prasathmani/tinyfilemanager
author: Prasath Mani
author_date: 2020-05-18 13:37:02+05:30
msg: Security fix #357
Download file causes timeout #353
Download Restart @ 88% #312
download large file issues #259
File upload issue #354
FIle Upload URL error #360
Bug in .tar archive extraction when destination(s) file(s) already exist (HTTP error 500) #332
Backup File return false #201
repository: repo_url: https://github.com/prasathmani/tinyfilemanager
repo_name: prasathmani/tinyfilemanager
description: Single-file PHP file manager, browser and manage your files efficiently and easily with tinyfilemanager
repo_language: PHP
owner: prasathmani
file_changes: filename: tinyfilemanager.php
change_type: ModificationType.MODIFY
diff: @@ -3,13 +3,13 @@
 $CONFIG = '{"lang":"en","error_reporting":false,"show_hidden":false,"hide_Cols":false,"calc_folder":false}';
 
 /**
- * H3K | Tiny File Manager V2.4.1
+ * H3K | Tiny File Manager V2.4.2
  * CCP Programmers | ccpprogrammers@gmail.com
  * https://tinyfilemanager.github.io
  */
 
 //TFM version
-define('VERSION', '2.4.1');
+define('VERSION', '2.4.2');
 
 //Application Title
 define('APP_TITLE', 'Tiny File Manager');
@@ -417,7 +417,7 @@ if (isset($_POST['ajax']) && !FM_READONLY) {
     //search : get list of files from the current folder
     if(isset($_POST['type']) && $_POST['type']=="search") {
         $dir = FM_ROOT_PATH;
-        $response = scan($_POST['path'], $_POST['content']);
+        $response = scan(fm_clean_path($_POST['path']), $_POST['content']);
         echo json_encode($response);
         exit();
     }
@@ -425,11 +425,16 @@ if (isset($_POST['ajax']) && !FM_READONLY) {
     // backup files
     if (isset($_POST['type']) && $_POST['type'] == "backup") {
         $file = $_POST['file'];
-        $path = $_POST['path'];
-        $date = date("dMy-His");
-        $newFile = $file . '-' . $date . '.bak';
-        copy($path . '/' . $file, $path . '/' . $newFile) or die("Unable to backup");
-        echo "Backup $newFile Created";
+        $dir = fm_clean_path($_POST['path']);
+        $path = FM_ROOT_PATH.'/'.$dir;
+        if($dir) {
+            $date = date("dMy-His");
+            $newFile = $file . '-' . $date . '.bak';
+            copy($path . '/' . $file, $path . '/' . $newFile) or die("Unable to backup");
+            echo "Backup $newFile Created";
+        } else {
+            echo "Error! Not allowed";
+        }
     }
 
     // Save Config
@@ -785,17 +790,7 @@ if (isset($_GET['dl'])) {
         $path .= '/' . FM_PATH;
     }
     if ($dl != '' && is_file($path . '/' . $dl)) {
-        header('Content-Description: File Transfer');
-        header('Content-Type: application/octet-stream');
-        header('Content-Disposition: attachment; filename="' . basename($path . '/' . $dl) . '"');
-        header('Content-Transfer-Encoding: binary');
-        header('Connection: Keep-Alive');
-        header('Expires: 0');
-        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
-        header('Pragma: public');
-        header('Content-Length: ' . filesize($path . '/' . $dl));
-        ob_end_clean();
-        readfile($path . '/' . $dl);
+        fm_download_file($path . '/' . $dl, $dl, 1024);
         exit;
     } else {
         fm_set_msg('File not found', 'error');
@@ -816,6 +811,10 @@ if (!empty($_FILES) && !FM_READONLY) {
     $errors = 0;
     $uploads = 0;
     $allowed = (FM_UPLOAD_EXTENSION) ? explode(',', FM_UPLOAD_EXTENSION) : false;
+    $response = array (
+        'status' => 'error',
+        'info'   => 'Oops! Try again'
+    );
 
     $filename = $f['file']['name'];
     $tmp_name = $f['file']['tmp_name'];
@@ -989,8 +988,17 @@ if (isset($_GET['unzip']) && !FM_READONLY) {
             $zipper = new FM_Zipper();
             $res = $zipper->unzip($zip_path, $path);
         } elseif ($ext == "tar") {
-            $gzipper = new PharData($zip_path);
-            $res = $gzipper->extractTo($path);
+            try {
+                $gzipper = new PharData($zip_path);
+                if (@$gzipper->extractTo($path,null, true)) {
+                    $res = true;
+                } else {
+                    $res = false;
+                }
+            } catch (Exception $e) {
+                //TODO:: need to handle the error
+                $res = true;
+            }
         }
 
         if ($res) {
@@ -1137,7 +1145,7 @@ if (isset($_GET['upload']) && !FM_READONLY) {
                     <?php echo lng('DestinationFolder') ?>: <?php echo fm_enc(fm_convert_win(FM_ROOT_PATH . '/' . FM_PATH)) ?>
                 </p>
 
-                <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]) . '?p=' . fm_enc(FM_PATH) ?>" class="dropzone card-tabs-container" id="fileUploader" enctype="multipart/form-data">
+                <form action="<?php echo htmlspecialchars(FM_SELF_URL) . '?p=' . fm_enc(FM_PATH) ?>" class="dropzone card-tabs-container" id="fileUploader" enctype="multipart/form-data">
                     <input type="hidden" name="p" value="<?php echo fm_enc(FM_PATH) ?>">
                     <input type="hidden" name="fullpath" id="fullpath" value="<?php echo fm_enc(FM_PATH) ?>">
                     <div class="fallback">
@@ -1707,7 +1715,7 @@ if (isset($_GET['edit'])) {
             </div>
             <div class="edit-file-actions col-xs-12 col-sm-7 col-lg-6 text-right pt-1">
                 <a title="Back" class="btn btn-sm btn-outline-primary" href="?p=<?php echo urlencode(trim(FM_PATH)) ?>&amp;view=<?php echo urlencode($file) ?>"><i class="fa fa-reply-all"></i> <?php echo lng('Back') ?></a>
-                <a title="Backup" class="btn btn-sm btn-outline-primary" href="javascript:backup('<?php echo urlencode($path) ?>','<?php echo urlencode($file) ?>')"><i class="fa fa-database"></i> <?php echo lng('BackUp') ?></a>
+                <a title="Backup" class="btn btn-sm btn-outline-primary" href="javascript:void(0);" onclick="backup('<?php echo urlencode(trim(FM_PATH)) ?>','<?php echo urlencode($file) ?>')"><i class="fa fa-database"></i> <?php echo lng('BackUp') ?></a>
                 <?php if ($is_text) { ?>
                     <?php if ($isNormalEditor) { ?>
                         <a title="Advanced" class="btn btn-sm btn-outline-primary" href="?p=<?php echo urlencode(trim(FM_PATH)) ?>&amp;edit=<?php echo urlencode($file) ?>&amp;env=ace"><i class="fa fa-pencil-square-o"></i> <?php echo lng('AdvancedEditor') ?></a>
@@ -2809,29 +2817,142 @@ function fm_get_onlineViewer_exts()
     return array('doc', 'docx', 'xls', 'xlsx', 'pdf', 'ppt', 'pptx', 'ai', 'psd', 'dxf', 'xps', 'rar', 'odt', 'ods');
 }
 
+function fm_get_file_mimes($extension)
+{
+    $fileTypes['swf'] = 'application/x-shockwave-flash';
+    $fileTypes['pdf'] = 'application/pdf';
+    $fileTypes['exe'] = 'application/octet-stream';
+    $fileTypes['zip'] = 'application/zip';
+    $fileTypes['doc'] = 'application/msword';
+    $fileTypes['xls'] = 'application/vnd.ms-excel';
+    $fileTypes['ppt'] = 'application/vnd.ms-powerpoint';
+    $fileTypes['gif'] = 'image/gif';
+    $fileTypes['png'] = 'image/png';
+    $fileTypes['jpeg'] = 'image/jpg';
+    $fileTypes['jpg'] = 'image/jpg';
+    $fileTypes['rar'] = 'application/rar';
+
+    $fileTypes['ra'] = 'audio/x-pn-realaudio';
+    $fileTypes['ram'] = 'audio/x-pn-realaudio';
+    $fileTypes['ogg'] = 'audio/x-pn-realaudio';
+
+    $fileTypes['wav'] = 'video/x-msvideo';
+    $fileTypes['wmv'] = 'video/x-msvideo';
+    $fileTypes['avi'] = 'video/x-msvideo';
+    $fileTypes['asf'] = 'video/x-msvideo';
+    $fileTypes['divx'] = 'video/x-msvideo';
+
+    $fileTypes['mp3'] = 'audio/mpeg';
+    $fileTypes['mp4'] = 'audio/mpeg';
+    $fileTypes['mpeg'] = 'video/mpeg';
+    $fileTypes['mpg'] = 'video/mpeg';
+    $fileTypes['mpe'] = 'video/mpeg';
+    $fileTypes['mov'] = 'video/quicktime';
+    $fileTypes['swf'] = 'video/quicktime';
+    $fileTypes['3gp'] = 'video/quicktime';
+    $fileTypes['m4a'] = 'video/quicktime';
+    $fileTypes['aac'] = 'video/quicktime';
+    $fileTypes['m3u'] = 'video/quicktime';
+
+    $fileTypes['php'] = ['application/x-php'];
+    $fileTypes['html'] = ['text/html'];
+    $fileTypes['txt'] = ['text/plain'];
+    return $fileTypes[$extension];
+}
+
 /**
  * This function scans the files and folder recursively, and return matching files
  * @param string $dir
+ * @param string $filter
  * @return json
  */
  function scan($dir, $filter = '') {
     $path = FM_ROOT_PATH.'/'.$dir;
-    $ite = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
-    $rii = new RegexIterator($ite, "/(".$filter.")/i");
-
-    $files = array(); 
-    foreach ($rii as $file) {
-        if (!$file->isDir()) {
-            $fileName = $file->getFilename();
-            $location = str_replace(FM_ROOT_PATH, '', $file->getPath());
-            $files[] = array(
-                    "name" => $fileName,
-                    "type" => "file",
-                    "path" => $location,
-                );           
-        }
-    }
-    return $files;
+     if($dir) {
+         $ite = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path));
+         $rii = new RegexIterator($ite, "/(" . $filter . ")/i");
+
+         $files = array();
+         foreach ($rii as $file) {
+             if (!$file->isDir()) {
+                 $fileName = $file->getFilename();
+                 $location = str_replace(FM_ROOT_PATH, '', $file->getPath());
+                 $files[] = array(
+                     "name" => $fileName,
+                     "type" => "file",
+                     "path" => $location,
+                 );
+             }
+         }
+         return $files;
+     }
+}
+
+/*
+Parameters: downloadFile(File Location, File Name,
+max speed, is streaming
+If streaming - videos will show as videos, images as images
+instead of download prompt
+https://stackoverflow.com/a/13821992/1164642
+*/
+
+function fm_download_file($fileLocation, $fileName, $chunkSize  = 1024)
+{
+    if (connection_status() != 0)
+        return (false);
+    $extension = pathinfo($fileName, PATHINFO_EXTENSION);
+
+    $contentType = fm_get_file_mimes($extension);
+    header("Cache-Control: public");
+    header("Content-Transfer-Encoding: binary\n");
+    header('Content-Type: $contentType');
+
+    $contentDisposition = 'attachment';
+
+
+    if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE")) {
+        $fileName = preg_replace('/\./', '%2e', $fileName, substr_count($fileName, '.') - 1);
+        header("Content-Disposition: $contentDisposition;filename=\"$fileName\"");
+    } else {
+        header("Content-Disposition: $contentDisposition;filename=\"$fileName\"");
+    }
+
+    header("Accept-Ranges: bytes");
+    $range = 0;
+    $size = filesize($fileLocation);
+
+    if (isset($_SERVER['HTTP_RANGE'])) {
+        list($a, $range) = explode("=", $_SERVER['HTTP_RANGE']);
+        str_replace($range, "-", $range);
+        $size2 = $size - 1;
+        $new_length = $size - $range;
+        header("HTTP/1.1 206 Partial Content");
+        header("Content-Length: $new_length");
+        header("Content-Range: bytes $range$size2/$size");
+    } else {
+        $size2 = $size - 1;
+        header("Content-Range: bytes 0-$size2/$size");
+        header("Content-Length: " . $size);
+    }
+
+    if ($size == 0) {
+        die('Zero byte file! Aborting download');
+    }
+    @ini_set('magic_quotes_runtime', 0);
+    $fp = fopen("$fileLocation", "rb");
+
+    fseek($fp, $range);
+
+    while (!feof($fp) and (connection_status() == 0)) {
+        set_time_limit(0);
+        print(@fread($fp, 1024*$chunkSize));
+        flush();
+        ob_flush();
+        sleep(1);
+    }
+    fclose($fp);
+
+    return ((connection_status() == 0) and !connection_aborted());
 }
 
 /**

programming_language: PHP
filename: translation.json
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,6 @@
 {
   "appName": "Tiny File Manager",
-  "version": "2.4.1",
+  "version": "2.4.2",
   "language": [
     {
       "name": "فارسی",

programming_language: JavaScript