严重性: 高危,CVSS 4.0 8.8;严重,CVSS 3.1 9.1(由 VulnCheck(CNA)指定)
向量(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
向量(v4.0): CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H
受影响版本: cornac < 2.6.0
修复版本: 2.6.0
CWE: CWE-22(对受限目录中路径名的不当限制,即“路径遍历”)
报告者: Rahul Karne 和 Bharath Kumar Reddy Janumpally
CNA: VulnCheck
发布时间: 2026 年 7 月 15 日
Cornac 的数据集加载器会自动下载并解压存档,而解压器信任存档中的每一个路径。
Cornac 是一个用于推荐系统的机器学习框架。其内置的数据集加载器会从网络获取存档并在没有任何确认步骤的情况下解压它们。cornac/utils/download.py 中的解压例程 _extract_archive() 调用了 archive.extractall(),且不对成员路径进行任何验证。因此,一个成员包含 ../ 序列、绝对路径或符号链接与硬链接条目的 TAR 存档,会将文件写入文件系统上的任意位置——即运行进程可写的任何地方——完全在预期的缓存目录之外。多个加载器通过纯 HTTP 获取数据,因此处于网络位置的攻击者可以在传输途中将存档替换为恶意存档,并在加载器被调用的瞬间将其解压。
任意文件写入,路径与内容完全由攻击者控制,仅受运行 Cornac 的进程权限限制。这是写入原语而非读取原语,因此不存在直接的信息泄露;但任意文件写入是通往代码执行和拒绝服务的成熟路径:
site-packages 中的 .py 文件、shell 启动文件或计划任务条目,会使攻击者代码在下次导入、下次启动 shell 或下次运行任务时执行。受影响对象: 任何使用 cornac < 2.6.0 并调用数据集加载器的场景,只要该加载器会下载并解压攻击者可控制或可拦截的存档。由于加载器会自动解压,因此除常规的加载器调用外无需任何额外步骤。
不受影响的对象:
2.6.0 或更高版本的用户,该版本在解压时会验证每个成员路径。| 指标 | 数值 | 来源 |
|---|---|---|
| 历史总下载量 | 4.1M | pepy.tech/projects/cornac |
| 最近 30 天下载量 | 67.8K | pepy.tech |
cornac/utils/download.py 中的 _extract_archive() 通过同一代码路径解压 ZIP 和 TAR 输入,并且在 TAR 的情况下调用 extractall() 时不进行任何路径检查:
# 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
成员名称不受任何约束,因此名为 ../../somewhere/file 的成员会解析到 extract_path 之外并被写入该处。
调用链从加载器调用开始即完全自动化:
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 的 zipfile 在解压时会清理 ../ 序列,因此在现代 Python 上,同一函数中的 ZIP 分支无法被利用。Python 的 tarfile 不会清理成员路径(3.12 之前如此,之后也仅在有显式过滤器时才清理)。由于 Cornac 通过相同的 extractall() 调用处理两种格式,ZIP 分支是安全的,而 TAR 分支通过同一行代码完全可被利用。这种不对称性很容易被忽略,正是因为代码在两种格式下看起来完全一致。
Cornac 使用 urllib.request.urlretrieve() 下载,没有证书固定,也没有存档完整性检查,并且多个内置加载器使用 http:// URL。处于网络位置的攻击者可以拦截纯 HTTP 下载并返回恶意 TAR,而无需入侵上游服务器,这正是该攻击向量属于远程且除加载器调用外无需任何用户交互的原因。
攻击者需要:
< 2.6.0 的目标。无需目标上的任何权限,也无需除加载器调用之外的任何用户交互。
以下内容通过直接调用 Cornac 自身的 _extract_archive,针对真实、未修改的包运行。
存在漏洞的版本(2.3.5)。 将一个包含 ../../ 成员的 TAR 解压到缓存目录中;该成员落在了该目录之外,并覆盖了属于另一个应用程序的文件:
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
验证结果:
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
良性成员按预期写入了缓存内部;../../ 成员则写入了另一个应用程序的目录中,现在导入该应用程序会失败。
完整攻击演示。 本仓库中的 poc_exploit.py 端到端运行完整攻击链:攻击者 HTTP 服务器提供恶意 TAR;一个复现 Cornac cache() 与 _extract_archive() 调用链的加载器从单次 load_feedback() 调用自动完成下载与解压;另一个应用程序目录中的两个文件被覆盖。存在漏洞的 _extract_archive() 直接取自 Cornac 源码并原样使用。
已修复版本(2.6.0)。 相同的遍历存档在写入任何内容之前即被拒绝:
ValueError: Blocked path traversal attempt in archive: ../victim/app.py
victim app.py after: def run(): return 'healthy' (unchanged)
升级到 cornac 2.6.0 或更高版本:
pip install --upgrade "cornac>=2.6.0"
在 2.6.0 中,未加防护的 extractall() 被替换为 _safe_extract() 辅助函数:它使用 os.path.realpath 解析每个成员路径,拒绝任何不在解压目录内的目标,并且仅允许常规文件和目录,阻止符号链接、硬链接和设备条目。
如果无法立即升级: 不要调用通过不可信渠道或纯 HTTP 渠道下载的数据集加载器,也不要从不受您控制的来源解压 TAR 存档。通过 HTTPS 获取可以降低传输途中被拦截的风险,但并不能消除来自恶意或已被入侵上游的风险。
VulnCheck(即 CNA)在 CVSS 4.0 下将本漏洞评定为 8.8(高危)(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:恶意存档通过网络投递,且多个加载器通过无完整性检查的纯 HTTP 获取。AC:L:构造遍历 TAR 轻而易举,攻击在每次调用时都能确定性地奏效。PR:N:攻击者无需在目标上拥有任何账户或立足点。UI:N:加载器自动下载并解压;单次加载器调用即可触发整条攻击链,无需任何确认步骤。VC:N:该原语仅可写入,因此不主张存在机密性影响。VI:H / VA:H:攻击者完全控制写入内容,并可破坏进程可写的文件,因此受影响系统的完整性和可用性均为高。SC:N / SI:N / SA:N:CNA 将影响评定为仅限于易受攻击系统自身的权限范围,而非一个独立的后续系统。CNA 评定的 8.8 分是此问题的权威评分。
| 日期 | 事件 |
|---|---|
| 2026 年 5 月 3 日 | 发现漏洞 |
由 Rahul Karne(安全研究员、IEEE 高级会员)和 Bharath Kumar Reddy Janumpally 发现并报告,并通过 VulnCheck 协调。Rahul 的相关披露还包括 CVE-2026-65321(PyAthena 中的 SQL 注入)和 CVE-2026-63720(datamodel-code-generator 中的代码注入)。
联系方式:[email protected] · GitHub:rahulreddykarne
8a50be7:https://github.com/PreferredAI/cornac/commit/8a50be72c11569b6747c6b96d6e31a0a1962f1a8媒体垂询:[email protected]。完整 PoC(攻击者服务器、遍历存档、受害应用程序)及更多技术细节可应要求提供。
| 典型用途 |
| 推荐系统研究与教学;数据集加载器自动通过网络获取,部分通过纯 HTTP |
| 框架固有特性 |
| 2026 年 5 月 4 日 | 报告(协同披露) |
| 2026 年 7 月 14 日 | 修复已合并(PR #709,提交 8a50be7) |
| 2026 年 7 月 15 日 | 发布已修复版本 2.6.0 |
| 2026 年 7 月 15 日 | VulnCheck 发布 CVE-2026-43637 |