CVE-2020-12103: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: 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.
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-28T22: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",