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
4 changes: 4 additions & 0 deletions newsfragments/+zip-special-mode-bits.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
``setuptools.archive_util`` no longer applies the setuid, setgid or sticky
bits recorded in a zip archive to the files it extracts, which includes
wheels unpacked by ``Wheel.install_as_egg``. The read, write and execute
permission bits are still honored.
3 changes: 2 additions & 1 deletion setuptools/archive_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def _unpack_zipfile_obj(zipfile_obj, extract_dir, progress_filter=default_filter
f.write(data)
unix_attributes = info.external_attr >> 16
if unix_attributes:
os.chmod(target, unix_attributes)
# honor the permission bits, but never setuid, setgid or sticky
os.chmod(target, unix_attributes & 0o777)


def _resolve_tar_file_or_dir(tar_obj, tar_member_obj):
Expand Down
21 changes: 21 additions & 0 deletions setuptools/tests/test_archive_util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import io
import stat
import sys
import tarfile
import zipfile

Expand Down Expand Up @@ -129,6 +131,25 @@ def test_unpack_zipfile_creates_directory_members(tmp_path):
assert (target / 'empty').is_dir()


@pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only')
def test_unpack_zipfile_drops_special_mode_bits(tmp_path):
"""
The setuid, setgid and sticky bits recorded for a zip member are not
applied to the extracted file, while its permission bits still are.
"""
special = stat.S_ISUID | stat.S_ISGID | stat.S_ISVTX
archive = tmp_path / 'special.zip'
with zipfile.ZipFile(archive, mode='w') as zf:
info = zipfile.ZipInfo('run.sh')
info.external_attr = (stat.S_IFREG | special | 0o755) << 16
zf.writestr(info, b'#!/bin/sh\n')
target = tmp_path / 'dest'

archive_util.unpack_archive(str(archive), str(target))

assert stat.S_IMODE((target / 'run.sh').stat().st_mode) == 0o755


@pytest.mark.skipif(not os_helper.can_symlink(), reason='Symlink support required')
def test_resolve_dest_rejects_symlinked_escape(tmp_path):
"""
Expand Down