PixelSmash: как 50-килобайтный видеофайл в папке роняет ваш сервер

Разбор CVE-2026-8461: heap out-of-bounds write в декодере MagicYUV (FFmpeg/libavcodec) из-за рассинхронизации slice_height с chroma vertical subsampling. Срабатывает не от клика по видео, а от генерации превью в файловом менеджере или сканирования библиотеки медиасервера — фикс в FFmpeg 8.1.2, но кто реально уязвим к RCE, а кто просто падает, сильно разнится. Вывод — про изоляцию декодера как рубеж обороны, который не зависит от скорости патчей.

Небольшой файл — по данным исследователей, около 50 КБ, — обычный .avi или .mkv, лежит в скачанной папке или в библиотеке медиасервера. Никто его не открывал, никто не нажимал play. И всё же декодер уже отработал: файловый менеджер построил превью, или медиасервер сам просканировал новую библиотеку. Именно на этом стыке — «декодер срабатывает раньше, чем человек успел что-то решить» — и живёт CVE-2026-8461, уязвимость в декодере MagicYUV из состава FFmpeg.

Важная оговорка сразу: «никто не открывал» — не значит «вообще ничего не происходило». CNA присвоил уязвимости вектор с UI:R, то есть «требуется взаимодействие пользователя». Но эту букву не стоит натягивать на все сценарии сразу: она описывает случай, когда действие совершает жертва — например, открывает папку в файловом менеджере, и тот сам строит превью. Серверный сценарий устроен иначе, и разбирать его нужно отдельно — ниже я развожу оба механизма, потому что риск у них разный.

Имя PixelSmash уязвимости дали сами исследователи — команда JFrog Security Research, нашедшая и раскрывшая баг; ни FFmpeg, ни NVD этим именем не оперируют. Сама уязвимость получила оценку CVSS 8.8 (HIGH) и CWE-787 (Out-of-bounds Write), пофикшена в FFmpeg 8.1.2. Дальше — как устроен баг на уровне декодера, кто и в какой степени уязвим, и почему для этого класса угроз патч — не единственная линия обороны.

PixelSmash: ретрофутуристский считыватель кинолент раскручивает с виду безобидную бобину плёнки; один кадр в окне считывателя даёт трещину, и кирпично-красный поток данных выплёскивается вниз, ломая аккуратную сетку буфера кадров и переливаясь за её границу

В статье

Тихий вектор: где вас декодируют без спроса

Полный CVSS-вектор уязвимости — CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H: сеть, низкая сложность атаки, без привилегий, требуется взаимодействие пользователя (UI:R), полное влияние на конфиденциальность/целостность/доступность. С трактовкой UI:R стоит быть аккуратным. По спецификации CVSS v3.1 это значит, что для срабатывания нужно действие пользователя, отличного от атакующего. Важно: действия самого атакующего сюда не считаются — загрузив файл на чужой сервер, он не «обеспечил взаимодействие пользователя».

Отсюда практический вывод: UI:R хорошо описывает desktop-сценарий — жертва открывает папку, и файловый менеджер сам строит превью. Действие жертвы есть, оно тривиально и уж точно не равно «открыть видео и нажать play». А вот серверный сценарий, где атакующий загрузил файл и сервер просканировал его сам, из этой буквы не выводится вовсе — там действия жертвы нет в принципе. Оценку присваивал CNA, и спорить с ней здесь смысла нет; правильнее не выводить свою модель угроз из одной буквы вектора, а посмотреть на конкретный конвейер: кто и в какой момент скармливает декодеру недоверенный файл.

На практике «тихий» вектор устроен через два разных механизма, и их стоит разделять — они не одно и то же:

(а) Desktop-thumbnailer. ffmpegthumbnailer — бэкенд генерации превью, который используют GNOME (Nautilus), KDE (Dolphin) и XFCE (Thunar) через thumbnailer-спецификацию freedesktop.org; он линкуется напрямую с libavcodec. Это самый буквальный сценарий «открыл папку — и всё уже случилось»: файловый менеджер сам запускает генерацию превью для видеофайлов в открытой директории, без единого клика по самому видео. Это архитектурно наиболее правдоподобный канал именно под формулировку «файл просто лежит в папке», хотя отдельного прямого теста именно этой цепочки нет — это вывод из устройства thumbnailer-инфраструктуры, а не протестированный кем-то PoC.

(б) Server-side library scan. Jellyfin, Nextcloud, Immich, PhotoPrism и подобные медиасерверы сами сканируют свою библиотеку — по расписанию или сразу после загрузки нового файла — и генерируют превью/метаданные. Это не «файл пассивно пролежал и что-то произошло», а активный процесс, который приложение запускает само: как только файл появляется в отслеживаемой директории, сервер сам инициирует декодирование. Итог для атакующего тот же — не нужно, чтобы кто-то вручную нажал play, — но механизм принципиально другой: это встроенная функция приложения, а не побочный эффект просмотра папки.

Оба механизма сходятся в одном: ни в (а), ни в (б) не требуется, чтобы жертва целенаправленно открыла видео. Разница — в том, откуда берётся запуск: в (а) это тривиальное действие жертвы (открыть папку), в (б) — собственная фоновая работа приложения, где действия жертвы может не быть вовсе. Поэтому оценивать риск стоит по своему конвейеру, а не по одной букве в CVSS-векторе.

Анатомия бага: MagicYUV и рассинхрон плоскостей

Уязвимость — heap out-of-bounds write (CWE-787) в декодере MagicYUV, libavcodec/magicyuv.c. В апстриме исправлена в 8.1.2 (и бэкпортом в ветке 8.0 — в 8.0.3); апстрим-сборки ниже этих порогов уязвимы. Для пакетов дистрибутивов и вендорских сборок номер версии ничего не решает — почему, разберу в разделе про аудит.

MagicYUV — lossless-кодек, и для YUV420-подобных форматов chroma-плоскости (цветоразностные каналы) должны иметь высоту вдвое меньше luma-плоскости (яркостного канала), с округлением вверх при нечётной высоте кадра. Проблема не в «высоте кадра» вообще, а конкретно в slice_height — высоте среза, которым декодер обрабатывает кадр по частям. Когда slice_height не выровнен по vshift (вертикальному сдвигу chroma-субдискретизации), при накоплении нескольких срезов декодер пишет на одну строку больше, чем реально выделено под chroma-буфер в куче. Отдельно стоит краевой случай однострочных MEDIAN-срезов (slice_height = 1 при interlaced-кадрах) — там декодер читал и писал за пределы буфера уже при самом MEDIAN-предсказании, без накопления по нескольким срезам.

flowchart TD F["файл .avi/.mkv/.mov (~50 КБ, по данным исследователей)"] --> P["probe: размеры кадра, chroma vertical subsampling"] P --> A["alloc chroma-плоскости:
высота выровнена по vshift"] P --> D["decode MagicYUV: накопление по slice_height,
не выровненному по vshift"] A --> B[("chroma-буфер кучи")] D -->|"пишет строку за границей буфера"| B B --> O[["heap out-of-bounds write (CWE-787)"]]

flowchart TD
  F["файл .avi/.mkv/.mov (~50 КБ, по данным исследователей)"] --> P["probe: размеры кадра, chroma vertical subsampling"]
  P --> A["alloc chroma-плоскости:
высота выровнена по vshift"] P --> D["decode MagicYUV: накопление по slice_height,
не выровненному по vshift"] A --> B[("chroma-буфер кучи")] D -->|"пишет строку за границей буфера"| B B --> O[["heap out-of-bounds write (CWE-787)"]]
MagicYUV: alloc chroma-буфера по выровненной высоте vs decode по невыровненному slice_height → запись за границей

Фикс — не один коммит, а три под одним PR (pr/23159), все указаны на ffmpeg.org/security.html и применены к master, к релизной ветке 8.1.2 и к 8.0.3:

Коммит (master) Автор Что делает
c23d4da3128c279b714b282e6ec292e8755007e3 Michael Niedermayer «Fix 1 line MEDIAN slices» — добавляет проверку if (1 + interlaced < height) перед обращением lefttop = left = dst[0], закрывая однострочный краевой случай
5806e8b9f34f1b0663b3017ef9dd1aa5d08116d1 michaelni, находка — Ori Hollander (JFrog) «Expand the s->interlaced slice-height sanity check» — было (s->slice_height >> s->vshift[1]) < 2, стало <= s->interlaced; в сообщении коммита прямо: «fixes a vulnerability in poc_magicyuv.avi», «out-of-array access»
374b726ffa878ee1cadb987bd1e1e20cc7ed8845 автор — orihjfrog (сам исследователь) «reject slice_height misaligned with chroma vshift» — явный reject с текстом ошибки «slice_height %d is not aligned to chroma vertical subsampling (must be a multiple of %d)»

Третий коммит и есть текстовое подтверждение формулировки бага: декодер и требования по высоте chroma-плоскости были рассинхронизированы именно на уровне slice_height, а не «высоты кадра» в целом.

От краша до RCE

Сразу оговорка: рабочего эксплойта, крафт-файла-PoC или шелл-кода в этой статье нет и не будет — материал только про то, как закрыть дыру.

Гарантировано здесь ровно одно — нарушение памяти: декодирование вредоносного файла всегда пишет за границу chroma-буфера, независимо от ASLR. А вот наблюдаемый эффект не гарантирован ничем и разнится от процесса к процессу: он зависит от состояния кучи, аллокатора и того, споткнётся ли glibc о повреждённые метаданные. Спектр — от немедленного падения (SIGABRT) до полного молчания, а при отдельных условиях, разобранных ниже, — до выполнения кода.

Но вот деталь, которая меняет модель угрозы, — порча кучи далеко не всегда видна. Jellyfin и Emby, по замерам JFrog, обрабатывают вредоносный файл с кодом возврата 0 и без единого сообщения об ошибке: сборки, инструментированные ASAN, подтверждают, что та же самая OOB-запись на 640 байт происходит, — просто продакшен-бинарник не спотыкается о проверку целостности glibc на испорченном чанке. Долгоживущий сервер может переварить сотни таких загрузок, накапливая повреждение кучи, и ни одна строчка в логах не скажет администратору, что его прямо сейчас атакуют. Это, пожалуй, худший из возможных режимов отказа: тишина вместо краша.

Полноценный контролируемый RCE — отдельная история. По данным JFrog, надёжный RCE продемонстрирован при отключённом ASLR (setarch -R): эксплойт использует захардкоженные адреса system() и командной строки, поэтому при включённой рандомизации он просто не попадает в цель. Исследователи отдельно нашли info-leak в декодере FlashSV (libavcodec/flashsv.c, живёт в апстриме с июля 2022 и на момент публикации не исправлен), но эксплуатируется он только при специфичных флагах запуска (например -threads 1), поэтому его собственная серьёзность — информационная. Про обход ASLR формулировка авторов осторожная: подобный info-leak, если такой найдут в будущем, в принципе можно было бы связать с этой OOB-записью и добиться RCE при полных мерах защиты — но такая связка требует отдельного исследования и не продемонстрирована. Практический вывод: при штатно включённом ASLR продемонстрированный эксплойт не срабатывает, но память всё равно повреждается — и это выливается либо в отказ, либо в тихую порчу кучи; при отключённом ASLR исследователи довели дело до выполнения кода. Ни один из этих исходов не является «безопасным».

Кто уязвим и что пропатчено

Разные вторичные источники после раскрытия перечисляли уязвимые приложения одним списком, без разделения по серьёзности. Это упрощение вводит в заблуждение: реального RCE добились не для всех. Ниже — статусы по состоянию на дату подготовки материала; вендорские данные (особенно вокруг конкретных приложений) сверяйте с первоисточником:

Приложение / компонент Статус Примечание
Jellyfin (10.11.9) RCE продемонстрирован (по данным JFrog) через обычный пайплайн сканирования медиатеки
Nextcloud RCE продемонстрирован (по данным JFrog) при включённой генерации видео-превью
Kodi, Emby, mpv, OBS Studio, Immich, PhotoPrism Подтверждён крах/DoS, RCE не показан источники прямо разделяют: «confirmed crashes… no lab-verified RCE»
NAS-устройства, Smart TV на FFmpeg Теоретически без конкретных производителей/моделей — недостаточно данных для утверждения как факта
qt5-qtwebengine / qt6-qtwebengine Подтверждено (Red Hat) QtWebEngine бандлит компоненты Chromium/FFmpeg — supply-chain-эффект шире исходного списка приложений
Plex Не затронут Собирает FFmpeg с --disable-decoders и минимальным allow-list. По оценке JFrog — «единственная эффективная защита, которую мы наблюдали», и единственный проект из всех протестированных, кто так делает
FFmpeg / libavcodec Пропатчено 8.1.2 (17.06.2026); те же 3 коммита также backport в 8.0.3

Технически уязвим сам libavcodec, но это не равно «любое приложение с FFmpeg внутри уязвимо на практике» — нужно, чтобы приложение реально прогоняло через MagicYUV-декодер недоверенные файлы на пути, доступном атакующему. Это условие выполняется у desktop-thumbnailer’ов и медиасерверов со сканированием библиотеки; для остальных программ, слинкованных с libavcodec, но не декодирующих недоверенный ввод автоматически, риск ниже.

Полезная практическая деталь: MagicYUV зарегистрирован для контейнеров AVI, MKV и MOV — тот же файл работает во всех трёх. А вот в MP4 кодек не зарегистрирован, поэтому этот контейнер вектором не является.

Отдельно стоит внимания реакция вендоров — она и делает эту историю показательной для цепочки поставок. Ни один из затронутых проектов баг не писал: он унаследован транзитивно, через зависимость от FFmpeg, и у большинства нет механизма самостоятельно его обнаружить или смягчить. Показателен ответ команды Nextcloud в программе bug bounty: они сочли обращение непроблемой на том основании, что уязвимость находится вне Nextcloud. Формально верно — и ровно поэтому дыра остаётся открытой: каждый участник цепочки показывает на апстрим, а недоверенный файл тем временем декодируется у него в конвейере. Ваша поверхность атаки включает каждую строку каждой зависимости, которую вы поставляете, — прочитали вы её или нет.

Аудит и защита своего конвейера

Ниже — только безопасные диагностические команды: определение версии, поиск точек, где декодер вызывается автоматически. Никакого крафта вредоносных файлов.

Начинать стоит не с версии, а с более прямого вопроса: собран ли у вас вообще уязвимый декодер. MagicYUV включён по умолчанию в каждой апстрим-сборке, поэтому ответ почти всегда «да» — но проверить дешевле, чем гадать:

ffmpeg -decoders 2>/dev/null | grep magicyuv
# строка вида "VFS..D magicyuv" — декодер присутствует, сборка подвержена
# пустой вывод — декодер выключен на этапе сборки, этот вектор закрыт

Важная поправка: эта команда отвечает только за тот ffmpeg, что лежит у вас в PATH, — а декодирует ваши файлы, скорее всего, совсем другой код. Jellyfin ходит в собственный jellyfin-ffmpeg, контейнеризованные сервисы — в свою сборку внутри образа, приложения на libav* — в слинкованную библиотеку, вообще минуя CLI. Поэтому запускать проверку нужно там, где живёт реальный декодер:

# внутри целевого контейнера, а не на хосте
docker exec <container> ffmpeg -decoders 2>/dev/null | grep magicyuv

# бандл приложения (пример Jellyfin) — свой бинарник мимо PATH
/usr/lib/jellyfin-ffmpeg/ffmpeg -decoders 2>/dev/null | grep magicyuv

# сервис линкуется с библиотекой и не зовёт CLI — проверяем саму библиотеку
strings -a /path/to/libavcodec.so.* 2>/dev/null | grep -c magicyuv

Если MagicYUV вам не нужен (а он нужен единицам — это lossless-кодек для монтажных конвейеров), самый радикальный способ закрыть вектор — вырезать декодер из сборки:

./configure --disable-decoder=magicyuv [остальные ваши флаги]

Это ровно тот подход, который спас Plex, — о нём ниже.

ffmpeg -version | head -1

И сразу важная оговорка, без которой эта проверка вводит в заблуждение. Номер версии решает вопрос только для сборок из апстрима: 8.1.2 и новее (либо 8.0.3+ из стабильной ветки) — фикс на месте, ниже — нет.

Для пакетов из дистрибутива это правило не работает. Debian, Ubuntu, RHEL и другие обычно не поднимают upstream-версию, а бэкпортят патч в свою — номер остаётся прежним, а дыра уже закрыта. В записи NVD, например, значится исправленный пакет Red Hat на базе FFmpeg 6.1.6: по «правилу 8.1.2» вы бы записали его в уязвимые, хотя он пропатчен. Ошибка в обе стороны неприятна: ложная тревога заставит чинить целое, а ложное спокойствие — пропустить настоящее.

Поэтому для дистрибутивных пакетов вердикт по номеру версии выносить нельзя — нужно смотреть changelog пакета и бюллетень вендора:

# Debian/Ubuntu: ищем упоминание CVE в changelog пакета
apt-get changelog ffmpeg 2>/dev/null | grep -i 'CVE-2026-8461'
zgrep -i 'CVE-2026-8461' /usr/share/doc/ffmpeg/changelog.Debian.gz 2>/dev/null

# RHEL/Fedora/CentOS: changelog rpm хранит список бэкпортнутых CVE
rpm -q --changelog ffmpeg 2>/dev/null | grep -i 'CVE-2026-8461'
rpm -q --changelog libavcodec 2>/dev/null | grep -i 'CVE-2026-8461'

# Пусто — это НЕ доказательство уязвимости: не все вендоры пишут CVE
# в changelog. Сверьтесь с security-бюллетенем своего дистрибутива.
# Кто из установленных thumbnailer'ов дёргает ffmpeg/libavcodec
grep -l ffmpegthumbnailer /usr/share/thumbnailers/*.thumbnailer 2>/dev/null

# Какие бинарники в системе слинкованы с libavcodec.
# readelf/objdump читают ELF-заголовки СТАТИЧЕСКИ, не запуская бинарник —
# в отличие от ldd, который дёргает динамический загрузчик и потому небезопасен
# на недоверенных файлах (см. предупреждение в man ldd).
for name in $(compgen -c | sort -u); do
  bin=$(command -v "$name" 2>/dev/null) || continue
  case "$bin" in /*) ;; *) continue ;; esac   # только реальные пути, не алиасы/функции
  readelf -d "$bin" 2>/dev/null | grep -q 'NEEDED.*libavcodec' && echo "$bin"
done | sort -u
docker exec <container> ffmpeg -version 2>/dev/null | head -1
# Если сервер бандлит собственную сборку ffmpeg (как Jellyfin) —
# проверять нужно версию внутри образа, а не версию хоста

Живой разбор аудита декодеров в конвейере — в стенде security/pixelsmash.

Оборона в глубину: почему песочница важнее гонки за патчами

Патч 8.1.2 закрывает конкретный баг в MagicYUV. Но конвейер «недоверенный файл → автоматический декодер» — это устройство, которое будет находить новые уязвимости и после этой. Полагаться на то, что очередной CVE в libavcodec выйдет раньше, чем кто-то воспользуется уязвимостью, — стратегия, которая структурно проигрывает: у атакующего есть время между публикацией уязвимости в апстриме и обновлением у конкретного вендора (thumbnailer, медиасервер, NAS-прошивка), а некоторые из этих цепочек обновляются медленно или не обновляются вовсе.

Правильный рубеж обороны — не патч сам по себе, а изоляция самого декодера: недоверенный файл должен парситься процессом без доступа к сети, домашнему каталогу и лишним правам, даже если в этом процессе есть баг. Это ровно то, что описано в Bubblewrap: песочница для недоверенного кода — непривилегированная песочница для одной команды, без демона и лишних прав, годится и для decoder-процесса thumbnailer’а или медиасервера. Если вы проектируете такой конвейер с нуля, стоит явно смоделировать эту поверхность атаки заранее — см. Моделирование угроз (STRIDE)Скоро: декодер недоверенного медиафайла — классическая граница доверия, которую легко упустить, если рисовать диаграмму потоков данных «по приложению», а не «по формату входных данных».

Итог

  • CVE-2026-8461 (медийное имя — PixelSmash) — heap out-of-bounds write (CWE-787) в декодере MagicYUV FFmpeg, libavcodec/magicyuv.c. CVSS 8.8 (HIGH), вектор AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H. Апстрим-фикс — 8.1.2 (бэкпорт в 8.0.3); для дистрибутивных и вендорских сборок вердикт по номеру версии выносить нельзя — только по advisory и changelog пакета.
  • Механика — рассинхрон slice_height с chroma vertical subsampling: накопление по невыровненным срезам пишет за границу chroma-буфера в куче; отдельный краевой случай — однострочные MEDIAN-срезы при interlaced. Фикс — 3 коммита под pr/23159, автор находки — Ori Hollander (JFrog).
  • Тихий вектор срабатывания — не «пассивное лежание файла», а два разных механизма: desktop-thumbnailer (открыл папку — сработал ffmpegthumbnailer) и server-side library scan (Jellyfin/Nextcloud/Immich/PhotoPrism сканируют свою библиотеку сами).
  • RCE (по данным JFrog) продемонстрирован на двух целях — Jellyfin 10.11.9 (через штатное сканирование медиатеки) и Nextcloud (через генерацию видео-превью), — и надёжно работает лишь при отключённом ASLR; у Kodi/Emby/mpv/OBS Studio/Immich/PhotoPrism подтверждён краш/DoS. Обход ASLR — открытое направление: связка с подобным info-leak не продемонстрирована.
  • Самое неприятное — тишина: Jellyfin и Emby переваривают вредоносный файл с кодом возврата 0 и без ошибок в логах, хотя куча испорчена. Сервер может копить повреждение сотнями загрузок, не подавая администратору ни одного сигнала.
  • Фикс — FFmpeg 8.1.2 (17.06.2026), backport также в 8.0.3. Если MagicYUV не нужен (а он нужен единицам), вектор режется на корню сборкой с --disable-decoder=magicyuv — именно этим спасся Plex. Но патч закрывает конкретный баг, а не саму конструкцию «недоверенный файл → автоматический декодер»: долгосрочная защита — изоляция декодера песочницей, а не гонка за очередным CVE.

Смежное на сайте: Bubblewrap: песочница для недоверенного кода — как ограничить процесс, который парсит недоверенные данные, ещё до того, как в нём найдут очередной баг; Моделирование угроз (STRIDE)Скоро — как заранее находить такие границы доверия на этапе проектирования, а не постфактум через CVE.

Источники

Обсуждение в Telegram

Присоединиться →

Комментарии