تجاوز وسيط Next.js (CVE-2025-29927): لماذا ينتمي التفويض إلى المسار لا إلى الحافة
في مارس 2025 كُشفت ثغرة حرجة في Next.js: يستطيع المهاجم تجاوز الوسيط بإرسال ترويسة HTTP واحدة. الثغرة CVE-2025-29927 بدرجة 9.1 أصابت التطبيقات التي تنفّذ فحوص المصادقة أو التفويض في وسيط Next.js. والدرس أكبر من التصحيح. الوسيط طبقة توجيه ومعالجة طلبات لا حدود تفويض، ومعاملته كذلك تحوّل ميزة راحة إلى نقطة فشل واحدة.
1. ماذا يفعل وسيط Next.js
يعمل الوسيط قبل اكتمال الطلب. ويمكنه إعادة الكتابة أو التوجيه أو إضافة ترويسات أو رفض الطلب. ومن الأنماط الشائعة:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
};
يبدو هذا كبوابة تحكم وصول، لكنه ليس كذلك. إنه مرشّح طلبات يعمل في العملية نفسها كالتطبيق، ويمكن تخطيه إذا اعتقد الإطار أن الطلب طلب فرعي داخلي.
2. ترويسة الطلب الفرعي الداخلي
يستخدم Next.js ترويسة داخلية x-middleware-subrequest لمنع التكرار اللانهائي عندما يطلق الوسيط طلبًا يعود إلى التطبيق. وتشفّر القيمة مسار الوسيط مكررًا حتى حد عمق. وفي الإصدارات المصابة كان الطلب الخارجي الذي يقدّم القيمة المتوقعة يُعامل كطلب فرعي داخلي ويُتخطى الوسيط كليًا.
# توضيح ضد نسخة اختبار مصابة ومصرّح باختبارها
curl -i https://target.example.com/dashboard \
-H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'
تعتمد القيمة الدقيقة على الإصدار وبنية المشروع (مثل src/middleware إذا كان الملف تحت src/). والحد خمسة. وإذا طابقت الترويسة فلا تُنفَّذ دالة الوسيط، ويمر الطلب مباشرة إلى المعالج أو الصفحة.
3. الإصدارات المصابة والإصلاحات
- Next.js 12: مصاب قبل 12.3.5؛ أُصلح في 12.3.5.
- Next.js 13: مصاب قبل 13.5.9؛ أُصلح في 13.5.9.
- Next.js 14: مصاب قبل 14.2.25؛ أُصلح في 14.2.25.
- Next.js 15: مصاب قبل 15.2.3؛ أُصلح في 15.2.3.
الإشعار هو GHSA-f82v-jwr5-mffw. ويمنع الإصلاح الطلبات الخارجية من تقديم الترويسة الداخلية، فلا يستطيع العميل تخطي الوسيط. والترقية ضرورية لكنها غير كافية إذا ظل التطبيق يعتمد على الوسيط وحده للتفويض.
4. لماذا الأثر خطير
تستخدم تطبيقات Next.js كثيرة الوسيط للفحوص الأهم:
- إعادة توجيه غير المصادَقين بعيدًا عن
/dashboard. - حجب
/adminما لم تحتوِ الجلسة دور مدير. - فرض عزل المستأجرين بإعادة الكتابة إلى خادم خاص.
- إضافة ترويسات أمان أو رفض مناطق محجوبة.
إذا تُخطي الوسيط فستُنفَّذ الصفحة أو المعالج. وقد تعرض صفحة لوحة معلومات تجلب بيانات من الخادم بيانات الضحية لطالب غير مصادَق. وقد تنفّذ نقطة API إدارية افترضت أن الوسيط تحقق من الدور إجراءات مميزة. لا تمنح الثغرة جلسة، بل تزيل الفحص الذي كان يُفترض أن يطلبها.
5. الاختبار بأمان
اختبر فقط تطبيقات تملكها أو مصرّحًا بتقييمها. تسلسل تحقق بسيط:
- اطلب مسارًا محميًا دون جلسة وتأكد من التوجيه أو 401/403 المتوقع.
- أعد الطلب مع ترويسة
x-middleware-subrequestوالقيمة المناسبة للإصدار. - قارن رموز الحالة وأجسام الاستجابة وترويسات
Location. - إذا ظهر المحتوى المحمي فتأكد أنه يحتوي بيانات لا ينبغي للمستخدم غير المصادَق رؤيتها.
- افحص هل يتحقق المعالج من الجلسة باستقلال. إن فعل فقد يقتصر التجاوز على العرض؛ وإن لم يفعل فالأثر أوسع.
# خط الأساس
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://target.example.com/admin
# فحص (لاختبار مصرّح به فقط)
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' \
-H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' \
https://target.example.com/admin
6. الإصلاح الدائم: فوّض في طبقة البيانات
يمكن أن يبقى الوسيط مفيدًا للتوجيه وكشف اللغة ومعالجة الترويسات، لكنه لا ينبغي أن يكون المكان الوحيد لاتخاذ قرار أمني. ضع فحص التفويض بجوار الوصول إلى البيانات:
// app/api/invoices/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
import { db } from '@/lib/db';
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
const session = await getSession(req);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const invoice = await db.invoice.findFirst({
where: { id: params.id, tenantId: session.tenantId }
});
if (!invoice) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(invoice);
}
هذا المعالج آمن حتى لو تُخطي الوسيط أو أُسيء ضبطه أو حُذف. فحص الجلسة ومرشّح المستأجر في الدالة نفسها التي تعيد البيانات. وهذه هي الخاصية المستهدفة: دفاع متعدد الطبقات مع الفحص المرجعي عند نقطة الاستخدام.
7. تحصين إضافي
- احجب الترويسة عند الحافة. كتخفيف مؤقت للنشر غير المصحح، أزل أو ارفض
x-middleware-subrequestمن كل الطلبات الخارجية عند الوكيل العكسي أو CDN. - صحّح كل نسخ Next.js. اشمل الاستضافة الذاتية وبيئات المعاينة والتطبيقات الداخلية. فنسخة staging قديمة تبقى نقطة دخول.
- احصر استخدام الوسيط. ابحث عن
middleware.tsوmiddleware.jsوsrc/middleware. ولكل مطابق اسأل: ماذا يحدث إذا لم تُنفَّذ هذه الدالة أبدًا؟ - أضف اختبارات ارتدادية. اختبر المسارات المحمية دون جلسة، وبجلسة منتهية، وبمستأجر آخر، وبوجود الترويسة الداخلية.
- سجّل قرارات التفويض. سجّل الموضوع المصادَق والمورد والقرار. وارتفاع مفاجئ في 200 على مسارات محمية دون جلسة فرصة كشف.
8. الخاتمة
تذكّرنا CVE-2025-29927 بأن الأطر تُحسّن تجربة المطوّر لا الثوابت الأمنية. الوسيط مكان أنيق للتعبير عن قواعد التوجيه، لكنه مكان هش لفرض التفويض. الترويسة التي سببت التجاوز كانت تفصيلًا داخليًا؛ والثغرة الحقيقية كانت افتراض أن مرشّح طلب لا يمكن تخطيه. صحّح الإطار، واحجب الترويسة، وانقل الفحص المرجعي إلى الدالة التي تقرأ المورد المحمي أو تعدّله فعلًا. وعندها يصبح الوسيط المتجاوز إعادة توجيه فائتة لا تسريب بيانات.