Skip to content

Commit d74d6b3

Browse files
committed
Prevent path traversal when installing wheels directly
1 parent 80a2a94 commit d74d6b3

File tree

2 files changed

+36
-0
lines changed

2 files changed

+36
-0
lines changed

src/pip/_internal/operations/install/wheel.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
3939
from pip._internal.utils.unpacking import (
4040
current_umask,
41+
is_within_directory,
4142
set_extracted_file_to_default_mode_plus_executable,
4243
zip_item_is_executable,
4344
)
@@ -537,12 +538,24 @@ def is_dir_path(path):
537538
# type: (RecordPath) -> bool
538539
return path.endswith("/")
539540

541+
def assert_no_path_traversal(dest_dir_path, target_path):
542+
# type: (text_type, text_type) -> None
543+
if not is_within_directory(dest_dir_path, target_path):
544+
message = (
545+
"The wheel {!r} has a file {!r} trying to install"
546+
" outside the target directory {!r}"
547+
)
548+
raise InstallationError(
549+
message.format(wheel_path, target_path, dest_dir_path)
550+
)
551+
540552
def root_scheme_file_maker(zip_file, dest):
541553
# type: (ZipFile, text_type) -> Callable[[RecordPath], File]
542554
def make_root_scheme_file(record_path):
543555
# type: (RecordPath) -> File
544556
normed_path = os.path.normpath(record_path)
545557
dest_path = os.path.join(dest, normed_path)
558+
assert_no_path_traversal(dest, dest_path)
546559
return ZipBackedFile(record_path, dest_path, zip_file)
547560

548561
return make_root_scheme_file
@@ -562,6 +575,7 @@ def make_data_scheme_file(record_path):
562575
_, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2)
563576
scheme_path = scheme_paths[scheme_key]
564577
dest_path = os.path.join(scheme_path, dest_subpath)
578+
assert_no_path_traversal(scheme_path, dest_path)
565579
return ZipBackedFile(record_path, dest_path, zip_file)
566580

567581
return make_data_scheme_file

tests/unit/test_wheel.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from mock import patch
1212
from pip._vendor.packaging.requirements import Requirement
1313

14+
from pip._internal.exceptions import InstallationError
1415
from pip._internal.locations import get_scheme
1516
from pip._internal.models.direct_url import (
1617
DIRECT_URL_METADATA_NAME,
@@ -458,6 +459,27 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir):
458459
assert not os.path.isdir(
459460
os.path.join(self.dest_dist_info, 'empty_dir'))
460461

462+
@pytest.mark.parametrize(
463+
"path",
464+
["/tmp/example", "../example", "./../example"]
465+
)
466+
def test_wheel_install_rejects_bad_paths(self, data, tmpdir, path):
467+
self.prep(data, tmpdir)
468+
wheel_path = make_wheel(
469+
"simple", "0.1.0", extra_files={path: "example contents\n"}
470+
).save_to_dir(tmpdir)
471+
with pytest.raises(InstallationError) as e:
472+
wheel.install_wheel(
473+
"simple",
474+
str(wheel_path),
475+
scheme=self.scheme,
476+
req_description="simple",
477+
)
478+
479+
exc_text = str(e.value)
480+
assert str(wheel_path) in exc_text
481+
assert "example" in exc_text
482+
461483

462484
class TestMessageAboutScriptsNotOnPATH(object):
463485

0 commit comments

Comments
 (0)