CVE-2013-1641 (published 2014-10-26T17:55Z)

Directory traversal vulnerability in the zip download functionality in QuiXplorer before 2.5.5 allows remote attackers to read arbitrary files via a .. (dot dot) in the selitems[] parameter in a download_selected action to index.php.

Severity: HIGH
CVSS v2 base score: 7.8 (AV:N/AC:L/Au:N/C:C/I:N/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/realtimeprojects/quixplorer
commit_details: repo_url: https://github.com/realtimeprojects/quixplorer
author: Claudio Klingler
author_date: 2013-11-03 10:28:10+01:00
msg: Bugfix: SECURITY FIX: download as zip may grant access to any files.

This bugfix removes a vulnerability bug of quixplorer from which
any file in your system (which is readable to the web process) may
be downloaded from your system.

- Refactored downloading and access right controlling.
- Fixed download and path handling.

Closes #21
repository: repo_url: https://github.com/realtimeprojects/quixplorer
repo_name: realtimeprojects/quixplorer
description: simply upload and download files to your webserver with a web-interface
repo_language: PHP
owner: realtimeprojects
file_changes: filename: fun_down.php
change_type: ModificationType.MODIFY
diff: @@ -1,43 +1,7 @@
 <?php
-/*------------------------------------------------------------------------------
-     The contents of this file are subject to the Mozilla Public License
-     Version 1.1 (the "License"); you may not use this file except in
-     compliance with the License. You may obtain a copy of the License at
-     http://www.mozilla.org/MPL/
 
-     Software distributed under the License is distributed on an "AS IS"
-     basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
-     License for the specific language governing rights and limitations
-     under the License.
-
-     The Original Code is fun_down.php, released on 2003-01-25.
-
-     The Initial Developer of the Original Code is The QuiX project.
-
-     Alternatively, the contents of this file may be used under the terms
-     of the GNU General Public License Version 2 or later (the "GPL"), in
-     which case the provisions of the GPL are applicable instead of
-     those above. If you wish to allow use of your version of this file only
-     under the terms of the GPL and not to allow others to use
-     your version of this file under the MPL, indicate your decision by
-     deleting  the provisions above and replace  them with the notice and
-     other provisions required by the GPL.  If you do not delete
-     the provisions above, a recipient may use your version of this file
-     under either the MPL or the GPL."
-------------------------------------------------------------------------------*/
-/*------------------------------------------------------------------------------
-Author: The QuiX project
-	quix@free.fr
-	http://www.quix.tk
-	http://quixplorer.sourceforge.net
-
-Comment:
-	QuiXplorer Version 2.3
-	File-Download Functions
-
-	Have Fun...
-------------------------------------------------------------------------------*/
-require_once("./_include/permissions.php");
+require_once("_include/fun_archive.php");
+require_once("_include/permissions.php");
 require_once("qxpage.php");
 
 /**
@@ -46,52 +10,42 @@ require_once("qxpage.php");
  **/
 function download_selected($dir)
 {
-    $dir = get_abs_dir($dir);
-    global $site_name;
     require_once("_include/fun_archive.php");
     $items = qxpage_selected_items();
-
-    // check if user selected any items to download
-    switch (count($items))
-    {
-        case 0:
-            show_error($GLOBALS["error_msg"]["miscselitems"]);
-        case 1:
-            if (is_file($items[0]))
-            {
-                download_item( $dir, $items[0] );
-                break;
-            }
-            // nobreak, downloading a directory is done
-            // with the zip file
-        default:
-            zip_download( $dir, $items );
-    }
+    _download_items($dir, $items);
 }
 
 // download file
 function download_item($dir, $item)
 {
-	// Security Fix:
-	$item=basename($item);
+    _download_items($dir, array($item));
+}
+
+function _download_items($dir, $items)
+{
+    // check if user selected any items to download
+    _debug("count items: '$items[0]'");
+    if (count($items) == 0)
+        show_error($GLOBALS["error_msg"]["miscselitems"]);
 
-	if (!permissions_grant($dir, $item, "read"))
-		show_error($GLOBALS["error_msg"]["accessfunc"]);
+    // check if user has permissions to download
+    // this file
+    if ( ! _is_download_allowed($dir, $items) )
+		show_error( $GLOBALS["error_msg"]["accessitem"] );
 
-	if (!get_is_file($dir,$item))
+    // if we have exactly one file and this is a real
+    // file we directly download it
+    if ( count($items) == 1 && get_is_file( $dir, $items[0] ) )
     {
-        _debug("error download");
-        show_error($item.": ".$GLOBALS["error_msg"]["fileexist"]);
+        $abs_item = get_abs_item($dir, $items[0]);
+        _download($abs_item, $items[0]);
     }
-	if (!get_show_item($dir, $item))
-        show_error($item.": ".$GLOBALS["error_msg"]["accessfile"]);
 
-	$abs_item = get_abs_item($dir,$item);
-    _download($abs_item, $item);
+    // otherwise we do the zip download
+    zip_download( get_abs_dir($dir), $items );
 }
 
-function _download_header($filename, $filesize = 0)
-{
+function _download_header($filename, $filesize = 0) {
 	$browser=id_browser();
 	header('Content-Type: '.(($browser=='IE' || $browser=='OPERA')?
 		'application/octetstream':'application/octet-stream'));
@@ -118,4 +72,17 @@ function _download($file, $localname)
 	exit;
 }
 
+function _is_download_allowed( $dir, $items )
+{
+    foreach ($items as $file)
+    {
+        if (!permissions_grant($dir, $file, "read"))
+            return false;
+
+        if (!get_show_item($dir, $file))
+            return false;
+    }
+
+    return true;
+}
 ?>

programming_language: PHP
filename: fun_extra.php
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,8 @@
 <?php
 
 require_once "_include/session.php";
+require_once "_include/qxpath.php";
+require_once "_include/str.php";
 
 //------------------------------------------------------------------------------
 // THESE ARE NUMEROUS HELPER FUNCTIONS FOR THE OTHER INCLUDE FILES
@@ -30,7 +32,7 @@ function get_abs_dir($path)
 }
 
 function get_abs_item($dir, $item) {		// get absolute file+path
-	return get_abs_dir($dir).DIRECTORY_SEPARATOR.$item;
+	return realpath(get_abs_dir($dir).DIRECTORY_SEPARATOR.$item);
 }
 /**
   get file relative from home
@@ -40,9 +42,15 @@ function get_rel_item($dir, $item)
     return $dir == "" ? $item : "$dir/$item";
 }
 
-function get_is_file($dir, $item) {		// can this file be edited?
-	return @is_file(get_abs_item($dir,$item));
+/**
+  can this file be edited?
+  */
+function get_is_file($dir, $item)
+{
+    $filename = get_abs_item($dir, $item);
+	return @is_file($filename);
 }
+
 //------------------------------------------------------------------------------
 function get_is_dir($dir, $item) {		// is this a directory?
 	return @is_dir(get_abs_item($dir,$item));
@@ -182,32 +190,46 @@ function get_mime_type ($dir, $item, $query)
  */
 function get_show_item ($directory, $file)
 {
+    // no relative paths are allowed in directories
     if ( preg_match( "/\.\./", $directory ) )
         return false;
 
-    if ( isset($file) && preg_match( "/\.\./", $file ) )
-        return false;
-
-    // dont display own directory
-    if ( $file == "." )
-        return false;
+    if ( isset($file) )
+    {
+        // file name must not contain any path separators
+        if ( preg_match( "/[\/\\\\]/", $file ) )
+            return false;
+
+        // dont display own and parent directory
+        if ( $file == "." || $file == ".." )
+            return false;
+
+        // determine full path to the file
+        $full_path = get_abs_item( $directory, $file );
+        _debug("full_path: $full_path");
+        if ( ! str_startswith( $full_path, path_f() ) )
+            return false;
+    }
 
-    if ( substr( $file, 0, 1) == "." && $GLOBALS["show_hidden"] == false )
-        return false;
+    // check if user is allowed to acces shidden files
+    global $show_hidden;
+    if ( ! $show_hidden )
+    {
+        if ( $file[0] == '.' )
+            return false;
+
+        // no part of the path may be hidden
+        $directory_parts = explode( "/", $directory );
+        foreach ( $directory_parts as $directory_part )
+        {
+            if ( $directory_part[0] == '.' )
+                return false;
+        }
+    }
 
     if (matches_noaccess_pattern($file))
         return false;
 
-    if ( $GLOBALS["show_hidden"] == false )
-    {
-      $directory_parts = explode( "/", $directory );
-      foreach ($directory_parts as $directory_part )
-      {
-        if ( substr ( $directory_part, 0, 1) == "." )
-          return false;
-      }
-    }
-
     return true;
 }
 

programming_language: PHP
filename: init.php
change_type: ModificationType.MODIFY
diff: @@ -17,15 +17,23 @@ if(isset($_SERVER)) {
 	die("<B>ERROR: Your PHP version is too old</B><BR>".
 	"You need at least PHP 4.0.0 to run QuiXplorer; preferably PHP 4.3.1 or higher.");
 }
-//------------------------------------------------------------------------------
-// Get Action
-if(isset($GLOBALS['__GET']["action"])) $GLOBALS["action"]=$GLOBALS['__GET']["action"];
-else $GLOBALS["action"]="list";
+
+_debug("xxx3 action: " . $GLOBALS['__GET']["action"] . "/" . $GLOBALS["__GET"]["do_action"] . "/" . (isset($GLOBALS['__GET']['action']) ? "true" : "false"));
+if (isset($GLOBALS['__GET']["action"]))
+{
+    $GLOBALS["action"]=$GLOBALS['__GET']["action"];
+}
+else
+{
+    $GLOBALS["action"]="list";
+}
 if($GLOBALS["action"]=="post" && isset($GLOBALS['__POST']["do_action"])) {
 	$GLOBALS["action"]=$GLOBALS['__POST']["do_action"];
 }
 if($GLOBALS["action"]=="") $GLOBALS["action"]="list";
 $GLOBALS["action"]=stripslashes($GLOBALS["action"]);
+_debug("xxx3 action: " . $GLOBALS['__GET']["action"] . "/" . $GLOBALS["__GET"]["do_action"] . "/" . (isset($GLOBALS['__GET']['action']) ? "true" : "false"));
+
 
 // Get Item
 if(isset($GLOBALS['__GET']["item"])) $GLOBALS["item"]=stripslashes($GLOBALS['__GET']["item"]);

programming_language: PHP
filename: permissions.php
change_type: ModificationType.MODIFY
diff: @@ -29,10 +29,10 @@ function permissions_get ()
   depending the rights of the current user.
 
   @param $dir	Directory in which  the action should happen. If this parameter is
-  		NULL the engine relys on the global permissions of the user.
+  		NULL the engine checks the global permissions of the user.
   		
   @param $file	File on which the action should happen, if this parameter is NULL
-  		the permission engine relys on the permission of the directory.
+  		the permission engine checks the user permissions on the directory.
 
   @param $action
   		One ore more action of the action set (see permissions_get) which sould

programming_language: PHP
filename: qxpath.php
change_type: ModificationType.MODIFY
diff: @@ -14,7 +14,7 @@
   user, since he should only see relative pathes.
  
  */
-function path_f ($path)
+function path_f ($path = '')
 {
     global $home_dir;
     $abs_dir = $home_dir;

programming_language: PHP
filename: str.php
change_type: ModificationType.ADD
diff: @@ -0,0 +1,8 @@
+<?php
+
+function str_startswith ( $candidate, $search_str )
+{
+    return substr( $candidate, 0, strlen( $search_str) ) == $search_str;
+}
+
+?>

programming_language: PHP
filename: index.php
change_type: ModificationType.MODIFY
diff: @@ -73,7 +73,11 @@ case "download":
 	ob_start(); // prevent unwanted output
 	require "./_include/fun_down.php";
 	ob_end_clean(); // get rid of cached unwanted output
-	download_item($current_dir, $GLOBALS["item"]);
+    global $item;
+    _debug("download item: $current_dir/$item");
+    if ($item == '' )
+        show_error($GLOBALS["error_msg"]["miscselitems"]);
+	download_item($current_dir, $item);
 	ob_start(false); // prevent unwanted output
 	exit;
 break;

programming_language: PHP