Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #3907 tar file placed outside of target location #6313

Merged
merged 2 commits into from
Sep 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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