
مختبر Docker معزول وماسح ضوئي Python غير تدميري يعيد إنتاج CVE-2026-94545، وهو حقن SVG في Next.js next/og ImageResponse، مع عناصر تحكم للمقارنة بين النسخة المصابة والنسخة المُرقّعة وأدلة PNG على مستوى البكسل.
next/og ImageResponse الخاص بـ Next.js (GHSA-vcvr-r3jv-pc5j)مادة بحث أمني لإعادة إنتاج والتحقق من CVE-2026-94545،
حقن تسلسل SVG في ImageResponse الخاص بـ Next.js (المنبع: Satori
GHSA-wx4j-mvgx-mqwp)، في مختبر معزول يعمل على loopback فقط.
>=16.2.0 <16.3.6 (Node.js ImageResponse فقط)26a52aff)؛ الإصدار 15.5.26 يتضمن
تقوية ذات صلة، والإصدار 15.x غير متأثر بمشكلة RCEاستخدمه فقط على الأنظمة التي تملكها أو المصرّح لك صراحةً باختبارها. أداة الفحص غير مدمّرة: فهي تُصيّر صورة وتعدّ البكسلات. لا يُكتب أو يُخزَّن أو يُنفَّذ أي شيء على الهدف.
الحالة: مُثبَت في المختبر المرفق.
مقابل نمط المسار الدقيق الوارد في الاستشارة (قيمة المهاجم تُمرَّر إلى
<svg><title>{value}</title></svg>):
next 16.3.5 (vulnerable): injected 500x150 red rect rendered into the PNG
(75,000 #FF0000 pixels measured in the response)
next 16.3.6 (patched) : value escaped, 0 red pixels
satori 0.25.0 (vuln) : payload present raw in the serialized SVG string
satori 0.33.5 (fixed) : payload escaped to </title>
هذا يُثبت بدائية الحقن من البداية إلى النهاية (المُسلسِل -> SVG المضمّن -> المخرجات المُصيّرة). وهو لا يُظهر تنفيذ شيفرة عن بُعد: إذ تربط الاستشارة RCE بمحلل SVG في الطبقة اللاحقة، الذي لم تُنشر تفاصيل ثغراته وقت كتابة هذا النص. لا يُدَّعى أي قراءة ملفات أو SSRF أو تنفيذ شيفرة.
يُسلسِل Satori عُقد <svg> عبر translateSVGNodeToSVGString()
(src/handler/preprocess.ts)، الذي كان يُدرِج القيم في مخرجات XML دون
تهريب:
String(node) - دون تهريب (الآن escapeXMLText)." تكسر السياق.style تُدرَج خامًا.&، فنجت كيانات XML الداخلية
من تحليل SVG الخارجي.expand.ts خصائص النمط الداخلية (بادئة _) إلى الأنماط
المُسلسَلة - وهي الآن مرفوضة.الإصلاح (satori@26a52aff)
يوجّه كل شيء عبر buildXMLString() المُقوّي الذي يُهرّب القيم
ويتحقق من أسماء XML. يتحقق هذا المختبر من كل ذلك سلوكيًا؛ الطلب الدقيق
وأدلة البكسلات موجودة في poc/.
المتطلبات: Docker مع Compose v2، وPython 3.10+ على المضيف (لأداة الفحص)، ووصول شبكي إلى npm عند التشغيل الأول.
./lab verify
يبني التطبيق المتأثر (next/og المُضمَّن من [email protected])، ويشترط
تصيير مستطيل العلامة في استجابة HTTP، ثم التطبيق المُصلَّح
([email protected]) ويشترط غيابه.
الضابط المتأثر:
[*] #FF0000 pixels: 75000 (threshold 10000)
[!!!] INJECTION CONFIRMED: the attacker value escaped <title> and
became a rendered SVG element in the generated image.
RESULT: VULNERABLE CONTROL CONFIRMED
الضابط المُصلَّح:
[*] #FF0000 pixels: 0 (threshold 10000)
[=] no injection observed
RESULT: PATCHED CONTROL CONFIRMED
./lab start vulnerable # 16.3.5 app on http://127.0.0.1:9481/og
./lab test # assert injection renders
./lab start patched # 16.3.6 app
./lab test # assert absence
./lab status
./lab reset
شغّل أداة الفحص يدويًا (الافتراضي loopback؛ الأهداف الخارجية المصرّح بها تحتاج التجاوز الصريح):
python3 poc/scan.py http://127.0.0.1:9481/og
python3 poc/scan.py https://host.example/og --param value \
--allow-authorized-non-loopback [--insecure]
الحمولة هي XML متوازن يحمل <rect> بحجم 500x150 أحمر خالص. على
نقطة نهاية متأثرة تهرب القيمة من <title> ويصبح المستطيل عنصرًا حقيقيًا
في مستند SVG المضمّن؛ تعدّ أداة الفحص بكسلات #FF0000 الدقيقة في ملف PNG
المُعاد (المتوقع 75,000؛ العتبة 10,000). على نقطة نهاية مُصلَّحة
تُهرَّب القيمة كنص ولا يُصيَّر أي أحمر. لا يغيّر المسبار شيئًا على الهدف:
طلب GET واحد، وتصيير صورة واحدة.
لاحظ أن الحامل هو عمدًا <rect> وليس نصًا: فالنص داخل
SVG المضمّن لا يملك خطًا قابلًا للتحميل في المُصيّر وسيكون غير مرئي
حتى عند حقنه.
إثبات هذه الثغرة هو محتوى البكسلات في صورة نقطية مُصيّرة،
ومطابِقات nuclei لا تستطيع فك ترميز ملفات PNG. تم بناء قالب تفاضلي
بالحجم ورُفض: على تطبيق مُصلَّح لا تزال الحمولة المُهرَّبة تُصيَّر (كنص
خامل حرفي)، لذا تختلف أحجام الحمولة مقابل الحمولة الحميدة على الأنظمة
المُصلَّحة أيضًا، والفصل بين فروق المتأثر والمُصلَّح هو
ضجيج ضغط خاص بالتطبيق. شحن مطابِق يُفعَّل على الأنظمة المُصلَّحة أسوأ
من عدم شحن أي شيء. استخدم poc/scan.py؛ فهو يقيّم صورة الاستجابة بدقة.
| الإصدار | التقييم |
|---|---|
| Next.js 16.3.5 (vendored @vercel/og) | أُعيد إنتاج الحقن من البداية إلى النهاية عبر HTTP |
| Next.js 16.3.6 (vendored @vercel/og) | أُعيد إنتاج الضابط السلبي المُصلَّح |
| satori 0.25.0 (npm) | حقن خام في SVG المُسلسَل (على مستوى المصدر) |
| satori 0.33.5 (npm، 22 سبتمبر 2026) | مُهرَّب (على مستوى المصدر) |
| Next.js 16.2.0-16.3.4 | متأثر وفق الاستشارة؛ لم يُختبر فرديًا |
| Next.js 15.x | غير متأثر بمشكلة RCE وفق مدونة المورّد |
.
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── docker-compose.yml
├── lab
├── app/
│ └── server.mjs # advisory route pattern over loopback HTTP
└── poc/
└── scan.py # non-destructive scanner (Python stdlib: urllib + zlib)
غير مُودَع في المستودع: assets/ (حزم next المضغوطة بـ npm + الحزم المستخرجة)،
node_modules/، .lab-state.
| الادعاء | الحالة |
|---|---|
| حقن تسلسل SVG في satori < الإصلاح | مُثبَت (المصدر + السلوك) |
| تصيير من البداية إلى النهاية للترميز المحقون عبر next/og 16.3.5 | مُثبَت |
| الضابط السلبي المُصلَّح (16.3.6 / satori 0.33.5) | مُثبَت |
| حاملات السمات/الأنماط (نفس السبب الجذري) | السبب الجذري مؤكَّد في فرق الإصلاح؛ حامل النص مُثبَت |
| SSRF / قراءة ملفات عبر مراجع موارد محقونة | لم يُختبر |
| تنفيذ شيفرة عن بُعد | غير مُدَّعى (يعتمد على تفاصيل محلل لاحق غير مُفصح عنها) |
شغّل هذا المستودع فقط على الأنظمة التي تملكها أو المصرّح لك صراحةً
باختبارها. يرفض مشغّل المختبر أي متغيرات غير vulnerable/patched،
وترفض أداة الفحص الأهداف غير loopback إلا إذا
تم توفير --allow-authorized-non-loopback، ومنفذ Compose مربوط
بـ 127.0.0.1.
راجع SECURITY.md للتعامل مع الإفصاح. لا يتضمن هذا المستودع أهداف إنتاج، أو بيانات عملاء، أو رموز وصول، أو أدلة من أنظمة حقيقية.
MIT - راجع LICENSE.