
CVE ID: CVE-2025-61686
الإصدارات المتأثرة: @react-router/node 7.0.0 إلى 7.9.3
نوع الثغرة: اجتياز المسار (Path Traversal) / عبور الدليل
توجد الثغرة في الدالة getFile() والمنطق المرتبط بعمليات الملفات داخل ملف packages/react-router-node/sessions/fileStorage.ts.
في السطر 267 من ملف packages/react-router/lib/server-runtime/sessions.ts:
async getSession(cookieHeader, options) {
let id = cookieHeader && (await cookie.parse(cookieHeader, options));
let data = id && (await readData(id));
return createSession(data || {}, id || "");
}
يتم استخراج Session ID من الكوكيز من خلال طريقة cookie.parse().
في الدالة decodeCookieValue() ضمن ملف packages/react-router/lib/server-runtime/cookies.ts:
async function decodeCookieValue(
value: string,
secrets: string[],
): Promise<any> {
if (secrets.length > 0) {
// 如果配置了 secrets,会验证签名
for (let secret of secrets) {
let unsignedValue = await unsign(value, secret);
if (unsignedValue !== false) {
return decodeData(unsignedValue);
}
}
return null; // 签名验证失败返回 null
}
// 如果没有配置 secrets(未签名),直接返回解码后的值
return decodeData(value);
}
المشكلة الأساسية: عندما تكون الكوكيز غير موقّعة (secrets مصفوفة فارغة أو غير مضبوطة)، تقوم decodeCookieValue بإرجاع قيمة الكوكيز بعد فك الترميز مباشرة، مما يتيح للمهاجم التحكم الكامل في هذه القيمة.
في ملف packages/react-router-node/sessions/fileStorage.ts:
export function getFile(dir: string, id: string): string {
// Divide the session id up into a directory (first 2 bytes) and filename
// (remaining 6 bytes) to reduce the chance of having very large directories,
return path.join(dir, id.slice(0, 4), id.slice(4));
}
تقوم هذه الدالة بتقسيم Session ID إلى جزأين:
id.slice(0, 4)id.slice(4)ثم يتم ربط المسار باستخدام path.join().
سيناريو الهجوم: عند استخدام createFileSessionStorage() وكانت الكوكيز غير موقّعة:
يمكن للمهاجم إنشاء Session ID خبيث، على سبيل المثال: ../../etc/passwd
معالجة الدالة getFile():
id.slice(0, 4) = ../.id.slice(4) = /etc/passwdpath.join(dir, ../., /etc/passwd)path.join() تعمل على تطبيع المسار، إلا أنه إذا كان dir مسارًا نسبيًا بالفعل أو بعد المعالجة، فقد يظل اجتياز المسار ممكنًاطريقة استغلال أكثر دقة:
....//etc/passwd
id.slice(0, 4) = ....id.slice(4) = //etc/passwd/etc/passwdأو:
../../../etc/passwd(16 حرفًا)
id.slice(0, 4) = ../.id.slice(4) = ./etc/passwdpath.join() إلى اجتياز المسارجميع عمليات الملفات التالية قد تتأثر:
readData(id) - عند قراءة بيانات الجلسة
async readData(id) {
try {
let file = getFile(dir, id);
let content = JSON.parse(await fsp.readFile(file, "utf-8"));
// ...
}
}
updateData(id, data, expires) - عند تحديث بيانات الجلسة
async updateData(id, data, expires) {
let content = JSON.stringify({ data, expires });
let file = getFile(dir, id);
await fsp.mkdir(path.dirname(file), { recursive: true });
await fsp.writeFile(file, content, "utf-8");
}
deleteData(id) - عند حذف بيانات الجلسة
async deleteData(id) {
try {
await fsp.unlink(getFile(dir, id));
}
}
يجب استيفاء جميع الشروط التالية في الوقت نفسه:
createFileSessionStorage()secrets في إعدادات cookie أو أن secrets مصفوفة فارغة)getSession(cookieHeader)
→ cookie.parse(cookieHeader)
→ decodeCookieValue(value, secrets) // 未签名时直接返回 value
→ readData(id)
→ getFile(dir, id) // 路径拼接,存在路径遍历风险
→ fsp.readFile(file) / fsp.writeFile(file) / fsp.unlink(file)
getFile() لا تتحقق من الوسيط id ولا تقوم بتطبيعهpath.join(): على الرغم من أن path.join() تطبّع المسار، إلا أنه في حالات معينة (مثل احتواء المسار على .. قبل الربط) قد يظل اجتياز المسار ممكنًا