CVE-2014-9767 (PHP, published 2016-05-22T01:59Z)
Description: value: Directory traversal vulnerability in the ZipArchive::extractTo function in ext/zip/php_zip.c in PHP before 5.4.45, 5.5.x before 5.5.29, and 5.6.x before 5.6.13 and ext/zip/ext_zip.cpp in HHVM before 3.12.1 allows remote attackers to create arbitrary empty directories via a crafted ZIP archive.
Severity: MEDIUM (CVSS v3 base score: 4.3)
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/facebook/hhvm
Files changed:
hphp/runtime/ext/zip/ext_zip.cpp
hphp/test/slow/ext_zlib/ziparchive_extractto_directory.php
hphp/test/slow/ext_zlib/ziparchive_extractto_directory.php.expect
hphp/test/slow/ext_zlib/ziparchive_extractto_file.php
hphp/test/slow/ext_zlib/ziparchive_extractto_file.php.expect
Commit message: ZipArchive::extractTo bug 70350

Summary:Don't allow upward directory traversal when extracting zip archive files.

Files in zip files with `..` or starting at main root `/` should be normalized
to something where the file being extracted winds up within the directory or
a subdirectory where the actual extraction is taking place.

http://git.php.net/?p=php-src.git;a=commit;h=f9c2bf73adb2ede0a486b0db466c264f2b27e0bb

Reviewed By: FBNeal

Differential Revision: D2798452

fb-gh-sync-id: 844549c93e011d1e991bb322bf85822246b04e30
shipit-source-id: 844549c93e011d1e991bb322bf85822246b04e30


Vulnerable code:
  auto sep = file.rfind('/');
    auto path = to + file.substr(0, sep);
    if (sep == file.length() - 1) {
  to.append(file);
  struct zip_stat zipStat;
  if (zip_stat(zip, file.c_str(), 0, &zipStat) != 0) {
    return false;
  }

Fixed code:
#include "hphp/runtime/base/file-util.h"
// Make the path relative to "." by flattening.
// This function is named the same and similar in implementation to that in
// php-src:php_zip.c
// One difference is that we canonicalize here whereas php-src is already
// assumed passed a canonicalized path.
static std::string make_relative_path(const std::string& path) {
  if (path.empty()) {
    return path;
  }

  // First get the path to a state where we don't have .. in the middle of it
  // etc. canonicalize handles Windows paths too.
  std::string canonical(FileUtil::canonicalize(path));

  // If we have a slash at the beginning, then just remove it and we are
  // relative. This check will hold because we have canonicalized the
  // path above to remove .. from the path, so we know we can be sure
  // we are at a good place for this check.
  if (FileUtil::isDirSeparator(canonical[0])) {
    return canonical.substr(1);
  }

  // If we get here, canonical looks something like:
  //   a/b/c

  // Search through the path and if we find a place where we have a slash
  // and a "." just before that slash, then cut the path off right there
  // and just take everything after the slash.
  std::string relative(canonical);
  int idx = canonical.length() - 1;
  while (1) {
    while (idx > 0 && !(FileUtil::isDirSeparator(canonical[idx]))) {
      idx--;
    }
    // If we ever get to idx == 0, then there were no other slashes to deal with
    if (idx == 0) {
      return canonical;
    }
    if (idx >= 1 && (canonical[idx - 1] == '.' || canonical[idx - 1] == ':')) {
      relative = canonical.substr(idx + 1);
      break;
    }
    idx--;
  }
  return relative;
}


  struct zip_stat zipStat;
  // Verify the file to be extracted is actually in the zip file
  if (zip_stat(zip, file.c_str(), 0, &zipStat) != 0) {
    return false;
  }

  auto clean_file = file;
  auto sep = std::string::npos;
  // Normally would just use std::string::rfind here, but if we want to be
  // consistent between Windows and Linux, even if techincally Linux won't use
  // backslash for a separator, we are checking for both types.
  int idx = file.length() - 1;
  while (idx >= 0) {
    if (FileUtil::isDirSeparator(file[idx])) {
      sep = idx;
      break;
    }
    idx--;
  }
    // make_relative_path so we do not try to put files or dirs in bad
    // places. This securely "cleans" the file.
    clean_file = make_relative_path(file);
    std::string path = to + clean_file;
    bool is_dir_only = true;
    if (sep < file.length() - 1) { // not just a directory
      auto clean_file_dir = HHVM_FN(dirname)(clean_file);
      path = to + clean_file_dir.toCppString();
      is_dir_only = false;
    }

    // Make sure the directory path to extract to exists or can be created
    // If we have a good directory to extract to above, we now check whether
    // the "file" parameter passed in is a directory or actually a file.
    if (is_dir_only) { // directory, like /usr/bin/
    // otherwise file is actually a file, so we actually extract.
  // We have ensured that clean_file will be added to a relative path by the
  // time we get here.
  to.append(clean_file);
<?php
$dir = tempnam(sys_get_temp_dir(), __FILE__);
unlink($dir);
mkdir($dir);
$archive = new ZipArchive();
$archive->open("$dir/a.zip",ZipArchive::CREATE);
$archive->addEmptyDir("../dir1/");
$archive->addEmptyDir("/var/www/dir2/");
$archive->addEmptyDir("a/b/../../../dir3");
$archive->addEmptyDir("a/b/../dir4/");
$archive->addEmptyDir("a/b/../c/../../d/dir5/");
$archive->addEmptyDir("./dir6");
$archive->addEmptyDir("x/y/dir7/..");
$archive->addEmptyDir("z/dir8/.");
$archive->addEmptyDir("simple");
$archive->close();
$archive2 = new ZipArchive();
$archive2->open("$dir/a.zip");
$archive2->extractTo($dir);
$archive2->close();
var_dump(file_exists("$dir/dir1/")); // true
var_dump(file_exists("../dir1/")); // false
var_dump(file_exists("$dir/var/www/dir2")); // true
var_dump(file_exists("/var/www/dir2/")); // false
var_dump(file_exists("$dir/dir3/")); // true
var_dump(file_exists("a/b/../../../dir3/")); // false
var_dump(file_exists("$dir/a/dir4/")); // true
var_dump(file_exists("a/b/../dir4/")); // false
var_dump(file_exists("$dir/d/dir5/")); // true
var_dump(file_exists("a/b/../c/../../d/dir5/")); // false
var_dump(file_exists("$dir/dir6")); // true
var_dump(file_exists("./dir6")); // false
var_dump(file_exists("$dir/x/y/")); // true
var_dump(file_exists("x/y/dir7/..")); // false
var_dump(file_exists("$dir/z/dir8")); // true
var_dump(file_exists("z/dir8/.")); // false
var_dump(file_exists("$dir/simple")); // true
var_dump(file_exists("simple")); // false

// Cleanup. Also verifies that everything is where it is supposed to be.
rmdir("$dir/dir1");
rmdir("$dir/var/www/dir2");
rmdir("$dir/var/www");
rmdir("$dir/var");
rmdir("$dir/dir3");
rmdir("$dir/a/dir4");
rmdir("$dir/a");
rmdir("$dir/d/dir5");
rmdir("$dir/d");
rmdir("$dir/dir6");
rmdir("$dir/x/y");
rmdir("$dir/x");
rmdir("$dir/z/dir8");
rmdir("$dir/z");
rmdir("$dir/simple");
unlink("$dir/a.zip");
rmdir($dir);
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
<?php
$str = 'temp';
$dir = tempnam(sys_get_temp_dir(), __FILE__);
unlink($dir);
mkdir($dir);
$archive = new ZipArchive();
$archive->open("$dir/a.zip",ZipArchive::CREATE);
$archive->addFromString("../dir1/A.txt", $str);
$archive->addFromString("/var/www/dir2/B", $str);
$archive->addFromString("a/b/../../../dir3/C.txt.other", $str);
$archive->addFromString("a/b/../dir4/D.not.a.file/D.a.file", $str);
$archive->addFromString("a/b/../c/../../d/dir5/E", $str);
$archive->addFromString("./dir6/F.exe", $str);
$archive->addFromString("./G.txt", $str);
$archive->addFromString("H.txt", $str);
$archive->addFromString("x/y/dir7/../I.txt", $str);
$archive->addFromString("z/dir8/./J.txt", $str);
$archive->addFromString("SIMPLE.txt", $str);
$archive->close();
$archive2 = new ZipArchive();
$archive2->open("$dir/a.zip");
$archive2->extractTo($dir);
$archive2->close();
var_dump(file_exists("$dir/dir1/A.txt")); // true
var_dump(file_exists("../dir1/A.txt")); // false
var_dump(file_exists("$dir/var/www/dir2/B")); // true
var_dump(file_exists("/var/www/dir2/B")); // false
var_dump(file_exists("$dir/dir3/C.txt.other")); // true
var_dump(file_exists("a/b/../../../dir3/C.txt.other")); // false
var_dump(file_exists("$dir/a/dir4/D.not.a.file/D.a.file")); // true
var_dump(file_exists("a/b/../dir4/D.not.a.file/D.a.file")); // false
var_dump(file_exists("$dir/d/dir5/E")); // true
var_dump(file_exists("a/b/../c/../../d/dir5/E")); // false
var_dump(file_exists("$dir/dir6/F.exe")); // true
var_dump(file_exists("./dir6/F.exe")); // false
var_dump(file_exists("$dir/G.txt")); // true
var_dump(file_exists("./G.txt")); // false
var_dump(file_exists("$dir/H.txt")); // true
var_dump(file_exists("H.txt")); // false
var_dump(file_exists("$dir/x/y/I.txt")); // true
var_dump(file_exists("x/y/dir7/../I.txt")); // false
var_dump(file_exists("$dir/z/dir8/J.txt")); // true
var_dump(file_exists("z/dir8/./J.txt")); // false
var_dump(file_exists("$dir/SIMPLE.txt")); // true
var_dump(file_exists("SIMPLE.txt")); // false

// Cleanup. Also verifies that everything is where it is supposed to be.
unlink("$dir/dir1/A.txt");
rmdir("$dir/dir1");
unlink("$dir/var/www/dir2/B");
rmdir("$dir/var/www/dir2");
rmdir("$dir/var/www");
rmdir("$dir/var");
unlink("$dir/dir3/C.txt.other");
rmdir("$dir/dir3");
unlink("$dir/a/dir4/D.not.a.file/D.a.file");
rmdir("$dir/a/dir4/D.not.a.file");
rmdir("$dir/a/dir4");
rmdir("$dir/a");
unlink("$dir/d/dir5/E");
rmdir("$dir/d/dir5");
rmdir("$dir/d");
unlink("$dir/dir6/F.exe");
rmdir("$dir/dir6");
unlink("$dir/x/y/I.txt");
rmdir("$dir/x/y");
rmdir("$dir/x");
unlink("$dir/z/dir8/J.txt");
rmdir("$dir/z/dir8");
rmdir("$dir/z");
unlink("$dir/G.txt");
unlink("$dir/H.txt");
unlink("$dir/a.zip");
unlink("$dir/SIMPLE.txt");
rmdir($dir);
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)