Skip to content

Commit

Permalink
Merge pull request #6313 from wilsonfv/master
Browse files Browse the repository at this point in the history
Fix #3907 tar file placed outside of target location
  • Loading branch information
chrahunt authored Sep 26, 2019
2 parents 3b7d2ee + 8c94b70 commit ea923d9
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 1 deletion.
2 changes: 2 additions & 0 deletions news/3907.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Abort installation if any archive contains a file which would be placed
outside the extraction location.
26 changes: 26 additions & 0 deletions src/pip/_internal/utils/unpacking.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ def has_leading_dir(paths):
return True


def is_within_directory(directory, target):
# type: ((Union[str, Text]), (Union[str, Text])) -> bool
"""
Return true if the absolute path of target is within the directory
"""
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)

prefix = os.path.commonprefix([abs_directory, abs_target])
return prefix == abs_directory


def unzip_file(filename, location, flatten=True):
# type: (str, str, bool) -> None
"""
Expand All @@ -107,6 +119,12 @@ def unzip_file(filename, location, flatten=True):
fn = split_leading_dir(name)[1]
fn = os.path.join(location, fn)
dir = os.path.dirname(fn)
if not is_within_directory(location, fn):
message = (
'The zip file ({}) has a file ({}) trying to install '
'outside target directory ({})'
)
raise InstallationError(message.format(filename, fn, location))
if fn.endswith('/') or fn.endswith('\\'):
# A directory
ensure_dir(fn)
Expand Down Expand Up @@ -166,6 +184,14 @@ def untar_file(filename, location):
# https://github.com/python/mypy/issues/1174
fn = split_leading_dir(fn)[1] # type: ignore
path = os.path.join(location, fn)
if not is_within_directory(location, path):
message = (
'The tar file ({}) has a file ({}) trying to install '
'outside target directory ({})'
)
raise InstallationError(
message.format(filename, path, location)
)
if member.isdir():
ensure_dir(path)
elif member.issym():
Expand Down
99 changes: 98 additions & 1 deletion tests/unit/test_utils_unpacking.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@
import shutil
import stat
import sys
import tarfile
import tempfile
import time
import zipfile

from pip._internal.utils.unpacking import untar_file, unzip_file
import pytest

from pip._internal.exceptions import InstallationError
from pip._internal.utils.unpacking import (
is_within_directory,
untar_file,
unzip_file,
)


class TestUnpackArchives(object):
Expand Down Expand Up @@ -71,6 +80,27 @@ def confirm_files(self):
"mode: %s, expected mode: %s" % (mode, expected_mode)
)

def make_zip_file(self, filename, file_list):
"""
Create a zip file for test case
"""
test_zip = os.path.join(self.tempdir, filename)
with zipfile.ZipFile(test_zip, 'w') as myzip:
for item in file_list:
myzip.writestr(item, 'file content')
return test_zip

def make_tar_file(self, filename, file_list):
"""
Create a tar file for test case
"""
test_tar = os.path.join(self.tempdir, filename)
with tarfile.open(test_tar, 'w') as mytar:
for item in file_list:
file_tarinfo = tarfile.TarInfo(item)
mytar.addfile(file_tarinfo, 'file content')
return test_tar

def test_unpack_tgz(self, data):
"""
Test unpacking a *.tgz, and setting execute permissions
Expand All @@ -90,3 +120,70 @@ def test_unpack_zip(self, data):
test_file = data.packages.joinpath("test_zip.zip")
unzip_file(test_file, self.tempdir)
self.confirm_files()

def test_unpack_zip_failure(self):
"""
Test unpacking a *.zip with file containing .. path
and expect exception
"""
files = ['regular_file.txt', os.path.join('..', 'outside_file.txt')]
test_zip = self.make_zip_file('test_zip.zip', files)
with pytest.raises(InstallationError) as e:
unzip_file(test_zip, self.tempdir)
assert 'trying to install outside target directory' in str(e.value)

def test_unpack_zip_success(self):
"""
Test unpacking a *.zip with regular files,
no file will be installed outside target directory after unpack
so no exception raised
"""
files = [
'regular_file1.txt',
os.path.join('dir', 'dir_file1.txt'),
os.path.join('dir', '..', 'dir_file2.txt'),
]
test_zip = self.make_zip_file('test_zip.zip', files)
unzip_file(test_zip, self.tempdir)

def test_unpack_tar_failure(self):
"""
Test unpacking a *.tar with file containing .. path
and expect exception
"""
files = ['regular_file.txt', os.path.join('..', 'outside_file.txt')]
test_tar = self.make_tar_file('test_tar.tar', files)
with pytest.raises(InstallationError) as e:
untar_file(test_tar, self.tempdir)
assert 'trying to install outside target directory' in str(e.value)

def test_unpack_tar_success(self):
"""
Test unpacking a *.tar with regular files,
no file will be installed outside target directory after unpack
so no exception raised
"""
files = [
'regular_file1.txt',
os.path.join('dir', 'dir_file1.txt'),
os.path.join('dir', '..', 'dir_file2.txt'),
]
test_tar = self.make_tar_file('test_tar.tar', files)
untar_file(test_tar, self.tempdir)


@pytest.mark.parametrize('args, expected', [
# Test the second containing the first.
(('parent/sub', 'parent/'), False),
# Test the first not ending in a trailing slash.
(('parent', 'parent/foo'), True),
# Test target containing `..` but still inside the parent.
(('parent/', 'parent/foo/../bar'), True),
# Test target within the parent
(('parent/', 'parent/sub'), True),
# Test target outside parent
(('parent/', 'parent/../sub'), False),
])
def test_is_within_directory(args, expected):
result = is_within_directory(*args)
assert result == expected

0 comments on commit ea923d9

Please sign in to comment.