Skip to content

Commit

Permalink
ZipArchive::extractTo bug 70350
Browse files Browse the repository at this point in the history
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
  • Loading branch information
paulbiss authored and Hhvm Bot committed Mar 1, 2016
1 parent 06f3fc8 commit 65c95a0
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 8 deletions.
95 changes: 87 additions & 8 deletions hphp/runtime/ext/zip/ext_zip.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <zip.h>

#include "hphp/runtime/base/array-init.h"
#include "hphp/runtime/base/file-util.h"
#include "hphp/runtime/base/preg.h"
#include "hphp/runtime/base/stream-wrapper-registry.h"
#include "hphp/runtime/ext/extension.h"
Expand Down Expand Up @@ -650,25 +651,103 @@ static bool HHVM_METHOD(ZipArchive, deleteName, const String& name) {
return true;
}

// 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;
}

static bool extractFileTo(zip* zip, const std::string &file, std::string& to,
char* buf, size_t len) {
auto sep = file.rfind('/');

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--;
}
if (sep != std::string::npos) {
auto path = to + file.substr(0, sep);
// 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 (!HHVM_FN(is_dir)(path) && !HHVM_FN(mkdir)(path, 0777, true)) {
return false;
}

if (sep == file.length() - 1) {
// 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/
return true;
}
// otherwise file is actually a file, so we actually extract.
}

to.append(file);
struct zip_stat zipStat;
if (zip_stat(zip, file.c_str(), 0, &zipStat) != 0) {
return false;
}
// We have ensured that clean_file will be added to a relative path by the
// time we get here.
to.append(clean_file);

auto zipFile = zip_fopen_index(zip, zipStat.index, 0);
FAIL_IF_INVALID_PTR(zipFile);
Expand Down
57 changes: 57 additions & 0 deletions hphp/test/slow/ext_zlib/ziparchive_extractto_directory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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);
18 changes: 18 additions & 0 deletions hphp/test/slow/ext_zlib/ziparchive_extractto_directory.php.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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)
75 changes: 75 additions & 0 deletions hphp/test/slow/ext_zlib/ziparchive_extractto_file.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?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);
22 changes: 22 additions & 0 deletions hphp/test/slow/ext_zlib/ziparchive_extractto_file.php.expect
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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)

0 comments on commit 65c95a0

Please sign in to comment.