CVE-2020-12102: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: In Tiny File Manager 2.4.1, there is a Path Traversal vulnerability in the ajax recursive directory listing functionality. This allows authenticated users to enumerate directories and files on the filesystem (outside of the application scope).
CWE 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.
CVSS v3 base score: 7.7
Published: 2020-04-28T21:15Z
Repository: https://github.com/prasathmani/tinyfilemanager
Language: PHP
Files changed: tinyfilemanager.php, translation.json
Commit date: 2020-05-18 13:37:02 +0530
Commit message: 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

Diff: diff --git a/tinyfilemanager.php b/tinyfilemanager.php
index 8b9a591..b661a9b 100644
--- a/tinyfilemanager.php
+++ b/tinyfilemanager.php
@@ -1,17 +1,17 @@
 <?php
 //Default Configuration
 $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');
 
 // --- EDIT BELOW CONFIGURATION CAREFULLY ---
@@ -415,23 +415,28 @@ 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();
     }
     
     // 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
     if (isset($_POST['type']) && $_POST['type'] == "settings") {
         global $cfg, $lang, $report_errors, $show_hidden_files, $lang_list, $hide_Cols, $calc_folder;
@@ -783,21 +788,11 @@ if (isset($_GET['dl'])) {
     $path = FM_ROOT_PATH;
     if (FM_PATH != '') {
         $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');
         fm_redirect(FM_SELF_URL . '?p=' . urlencode(FM_PATH));
     }
@@ -814,10 +809,14 @@ 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'];
     $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
     $isFileAllowed = ($allowed) ? in_array($ext, $allowed) : true;
@@ -987,12 +986,21 @@ if (isset($_GET['unzip']) && !FM_READONLY) {
 
         if($ext == "zip") {
             $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) {
             fm_set_msg('Archive unpacked');
         } else {
@@ -1135,11 +1143,11 @@ if (isset($_GET['upload']) && !FM_READONLY) {
                 <p class="card-text">
                     <a href="?p=<?php echo FM_PATH ?>" class="float-right"><i class="fa fa-chevron-circle-left go-back"></i> <?php echo lng('Back')?></a>
                     <?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">
                         <input name="file" type="file" multiple/>
                     </div>
@@ -1705,11 +1713,11 @@ if (isset($_GET['edit'])) {
                     <?php } ?>
                 </div>
             </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>
                         <button type="button" class="btn btn-sm btn-outline-primary name="Save" data-url="<?php echo fm_enc($file_url) ?>" onclick="edit_save(this,'nrl')"><i class="fa fa-floppy-o"></i> Save
                         </button>
@@ -2807,33 +2815,146 @@ function fm_get_text_names()
 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());
 }
 
 /**
  * Class to work with zip files (using ZipArchive)
  */
diff --git a/translation.json b/translation.json
index c10fdb1..ca63b28 100644
--- a/translation.json
+++ b/translation.json
@@ -1,8 +1,8 @@
 {
   "appName": "Tiny File Manager",
-  "version": "2.4.1",
+  "version": "2.4.2",
   "language": [
     {
       "name": "فارسی",
       "code": "Fa",
       "translation": {

Vulnerable code:  * H3K | Tiny File Manager V2.4.1
define('VERSION', '2.4.1');
        $response = scan($_POST['path'], $_POST['content']);
        $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";
        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);
            $gzipper = new PharData($zip_path);
            $res = $gzipper->extractTo($path);
                <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]) . '?p=' . fm_enc(FM_PATH) ?>" class="dropzone card-tabs-container" id="fileUploader" enctype="multipart/form-data">
                <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>
    $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;
  "version": "2.4.1",
Fixed code:  * H3K | Tiny File Manager V2.4.2
define('VERSION', '2.4.2');
        $response = scan(fm_clean_path($_POST['path']), $_POST['content']);
        $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";
        }
        fm_download_file($path . '/' . $dl, $dl, 1024);
    $response = array (
        'status' => 'error',
        'info'   => 'Oops! Try again'
    );
            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;
            }
                <form action="<?php echo htmlspecialchars(FM_SELF_URL) . '?p=' . fm_enc(FM_PATH) ?>" class="dropzone card-tabs-container" id="fileUploader" enctype="multipart/form-data">
                <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>
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];
}

 * @param string $filter
     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());
  "version": "2.4.2",