Skip to content
Open
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
3 changes: 3 additions & 0 deletions docs/news.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ Release Notes

**UNRELEASED**

- Fixed ``wheel pack`` writing the new wheel outside the destination directory when
the ``Build`` header in the ``WHEEL`` file of the directory being packed contained
path separators (path traversal)
- Fixed ``wheel unpack`` taking the mode of the unpack directory itself from the
archive when a wheel contains a member whose name is made up entirely of empty,
``.`` or ``..`` path components (such as ``../``), which let a malicious wheel
Expand Down
8 changes: 7 additions & 1 deletion src/wheel/_commands/pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ def pack(
tagline = compute_tagline(tags)

# Repack the wheel
wheel_path = os.path.join(dest_dir, f"{name_version}-{tagline}.whl")
wheel_name = f"{name_version}-{tagline}.whl"
if os.path.basename(wheel_name) != wheel_name:
# the build number and tags come from the WHEEL file and may contain path
# separators; never write outside the destination directory
raise WheelError(f"Invalid build number or tags in {dist_info_dir}/WHEEL")

wheel_path = os.path.join(dest_dir, wheel_name)
with WheelFile(wheel_path, "w") as wf:
print(f"Repacking wheel as {wheel_path}...", end="", flush=True)
wf.write_files(directory)
Expand Down
28 changes: 28 additions & 0 deletions tests/commands/test_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from pytest import TempPathFactory

from wheel._commands import main
from wheel._commands.pack import pack
from wheel.wheelfile import WheelError

from .util import run_command

Expand Down Expand Up @@ -255,3 +257,29 @@ def test_pack_invalid_build_tag(
exc = exc_info.value
assert exc.returncode == 2
assert f"error: argument --build-number: {error}" in exc.stderr


def test_pack_rejects_build_number_path_traversal(
tmp_path_factory: TempPathFactory, tmp_path: Path
) -> None:
# A build number in the WHEEL file that contains path separators must not get
# the wheel written outside the destination directory.
unpack_dir = tmp_path_factory.mktemp("wheeldir")
with ZipFile(TESTWHEEL_PATH) as zf:
zf.extractall(unpack_dir)

wheel_file_path = unpack_dir.joinpath("test-1.0.dist-info").joinpath("WHEEL")
wheel_file_content = wheel_file_path.read_bytes()
assert b"Build" not in wheel_file_content
wheel_file_content += b"Build: 1/../../outside/pwned-1.0\r\n"
wheel_file_path.write_bytes(wheel_file_content)

outside = tmp_path.joinpath("outside")
outside.mkdir()
dest_dir = tmp_path.joinpath("dest")
dest_dir.joinpath("test-1.0-1").mkdir(parents=True)

with pytest.raises(WheelError):
pack(str(unpack_dir), str(dest_dir), None)

assert not any(outside.iterdir())
Loading