CVE-2013-1641 (PHP, published 2014-10-26T17:55Z)
Description: value: 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 v3 base score: )
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
Repository: https://github.com/realtimeprojects/quixplorer
Files changed:
src/_include/fun_down.php
src/_include/fun_extra.php
src/_include/init.php
src/_include/permissions.php
src/_include/qxpath.php
src/_include/str.php
src/index.php
Commit message: 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


Vulnerable code:
/*------------------------------------------------------------------------------
     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");
    $dir = get_abs_dir($dir);
    global $site_name;

    // 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 );
    }
	// Security Fix:
	$item=basename($item);
	if (!permissions_grant($dir, $item, "read"))
		show_error($GLOBALS["error_msg"]["accessfunc"]);
	if (!get_is_file($dir,$item))
        _debug("error download");
        show_error($item.": ".$GLOBALS["error_msg"]["fileexist"]);
	if (!get_show_item($dir, $item))
        show_error($item.": ".$GLOBALS["error_msg"]["accessfile"]);
	$abs_item = get_abs_item($dir,$item);
    _download($abs_item, $item);
function _download_header($filename, $filesize = 0)
{
	return get_abs_dir($dir).DIRECTORY_SEPARATOR.$item;
function get_is_file($dir, $item) {		// can this file be edited?
	return @is_file(get_abs_item($dir,$item));
    if ( isset($file) && preg_match( "/\.\./", $file ) )
        return false;

    // dont display own directory
    if ( $file == "." )
        return false;
    if ( substr( $file, 0, 1) == "." && $GLOBALS["show_hidden"] == false )
        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;
      }
    }

//------------------------------------------------------------------------------
// Get Action
if(isset($GLOBALS['__GET']["action"])) $GLOBALS["action"]=$GLOBALS['__GET']["action"];
else $GLOBALS["action"]="list";
  		NULL the engine relys on the global permissions of the user.
  		the permission engine relys on the permission of the directory.
function path_f ($path)
	download_item($current_dir, $GLOBALS["item"]);

Fixed code:
require_once("_include/fun_archive.php");
require_once("_include/permissions.php");
    _download_items($dir, $items);
    _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"]);
    // check if user has permissions to download
    // this file
    if ( ! _is_download_allowed($dir, $items) )
		show_error( $GLOBALS["error_msg"]["accessitem"] );
    // 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] ) )
        $abs_item = get_abs_item($dir, $items[0]);
        _download($abs_item, $items[0]);
    // otherwise we do the zip download
    zip_download( get_abs_dir($dir), $items );
function _download_header($filename, $filesize = 0) {
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;
}
require_once "_include/qxpath.php";
require_once "_include/str.php";
	return realpath(get_abs_dir($dir).DIRECTORY_SEPARATOR.$item);
/**
  can this file be edited?
  */
function get_is_file($dir, $item)
{
    $filename = get_abs_item($dir, $item);
	return @is_file($filename);

    // no relative paths are allowed in directories
    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;
    }
    // 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;
        }
    }

_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";
}
_debug("xxx3 action: " . $GLOBALS['__GET']["action"] . "/" . $GLOBALS["__GET"]["do_action"] . "/" . (isset($GLOBALS['__GET']['action']) ? "true" : "false"));

  		NULL the engine checks the global permissions of the user.
  		the permission engine checks the user permissions on the directory.
function path_f ($path = '')
<?php

function str_startswith ( $candidate, $search_str )
{
    return substr( $candidate, 0, strlen( $search_str) ) == $search_str;
}

?>
    global $item;
    _debug("download item: $current_dir/$item");
    if ($item == '' )
        show_error($GLOBALS["error_msg"]["miscselitems"]);
	download_item($current_dir, $item);