From f567f1be4c2cbcb43d54d9417d85c303abac28ca Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Jan 2026 20:09:03 -0500 Subject: [PATCH 1/9] Add repro as provided by tsigouris007 --- tests/test_safety.py | 146 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 tests/test_safety.py Index: jaraco.context-5.3.0/tests/test_safety.py =================================================================== --- /dev/null +++ jaraco.context-5.3.0/tests/test_safety.py @@ -0,0 +1,72 @@ +import io +import sys +import types +from contextlib import nullcontext as does_not_raise + +import pytest + +import jaraco.context +from jaraco.context import tarfile + + +def make_tarball_with(member): + tar_data = io.BytesIO() + with tarfile.open(fileobj=tar_data, mode='w') as tar: + tarinfo = tarfile.TarInfo(name=member.path) + content = f'content for {member.path}' + bin_content = content.encode('ascii') + tarinfo.size = len(bin_content) + tar.addfile(tarinfo, io.BytesIO(bin_content)) + + tar_data.seek(0) + return tar_data + + +cases = [ + types.SimpleNamespace( + path='dummy_dir/legitimate_file.txt', + expect=does_not_raise(), + ), + pytest.param( + types.SimpleNamespace( + path='dummy_dir/subdir/../legitimate_file.txt', + expect=does_not_raise(), + ), + marks=pytest.mark.skipif( + (3, 11) < sys.version_info < (3, 13), + reason='Fails with FileExistsError on Python 3.12', + ), + ), + types.SimpleNamespace( + path='dummy_dir/../../tmp/pwned_by_zipslip.txt', + expect=pytest.raises(tarfile.OutsideDestinationError), + ), + types.SimpleNamespace( + path='dummy_dir/../../../../home/pwned_home.txt', + expect=pytest.raises(tarfile.OutsideDestinationError), + ), + types.SimpleNamespace( + path='dummy_dir/../escaped.txt', + expect=pytest.raises(tarfile.OutsideDestinationError), + ), +] + + +@pytest.fixture(params=cases) +def tarfile_case(request): + with tarfile.open(fileobj=make_tarball_with(request.param), mode='r') as tf: + yield types.SimpleNamespace( + tarfile=tf, + expect=request.param.expect, + ) + + +def test_zipslip_exploit(tmp_path, tarfile_case): + """ + Ensure that protections from the default tarfile filter are applied. + """ + (member,) = tarfile_case.tarfile + with tarfile_case.expect: + tarfile_case.tarfile.extract( + member, path=tmp_path, filter=jaraco.context._default_filter + ) Index: jaraco.context-5.3.0/jaraco/context.py =================================================================== --- jaraco.context-5.3.0.orig/jaraco/context.py +++ jaraco.context-5.3.0/jaraco/context.py @@ -62,12 +62,19 @@ def tarball( try: req = urllib.request.urlopen(url) with tarfile.open(fileobj=req, mode='r|*') as tf: - tf.extractall(path=target_dir, filter=strip_first_component) + tf.extractall(path=target_dir, filter=_default_filter) yield target_dir finally: shutil.rmtree(target_dir) +def _compose_tarfile_filters(*filters): + def compose_two(f1, f2): + return lambda member, path: f1(f2(member, path), path) + + return functools.reduce(compose_two, filters, lambda member, path: member) + + def strip_first_component( member: tarfile.TarInfo, path, @@ -76,6 +83,9 @@ def strip_first_component( return member +_default_filter = _compose_tarfile_filters(tarfile.data_filter, strip_first_component) + + def _compose(*cmgrs): """ Compose any number of dependent context managers into a single one.