|
|
|
|
@@ -0,0 +1,122 @@
|
|
|
|
|
From f567f1be4c2cbcb43d54d9417d85c303abac28ca Mon Sep 17 00:00:00 2001
|
|
|
|
|
From: "Jason R. Coombs" <jaraco@jaraco.com>
|
|
|
|
|
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.
|