الأمن
الـ middleware ليس authorization
4 ثغرات في NestJS خلال 7 أشهر، وكلها ثغرة واحدة: الـ matcher والـ handler اختلفا على العنوان نفسه. مكان التحقق هو حيث تُقرأ البيانات.
وجدنا هذا في موقعنا نحن، ولهذا نستطيع وصفه بصدق.
يدمج App Router في Next.js الـ metadata نزولاً في شجرة الـ routes، مفتاحاً مفتاحاً. يصدّر الـ layout بعض القيم، وتصدّر الـ page بعضاً آخر، فيدمجها الـ framework: قيم الـ page تفوز حيث يعرّف الاثنان المفتاح نفسه، وقيم الـ layout تبقى حيث تسكت الـ page. هذا سلوك موثَّق ومريح، إلى أن يكون المفتاح المعني هو alternates.
كان layout الـ locale عندنا يضبط alternates للصفحة الرئيسية، لأن الرئيسية هي ما يعرضه عند /ar. فكل page تحته لم تصدّر alternates خاصاً بها ورثت ذلك الكائن كاملاً. وهكذا كانت /ar/pricing و/ar/services وroutes الـ /ar/services/[slug] التسع ونظائرها الإنكليزية تُرسل جميعاً:
<link rel="canonical" href="https://qasioun.cloud/ar">24 عنواناً، كل واحد يخبر Google أنه نسخة مكررة من الصفحة الرئيسية ولا ينبغي فهرسته وحده. ولم يكن شيء معطلاً: كل page تُعرض كما ينبغي، وتُعيد 200، وتظهر في الـ sitemap. كان الموقع يطلب بهدوء أن يُشطَب من الفهرس.
الـ canonical الخاطئ لا ينتج خطأً ولا تحذيراً ولا عَرَضاً مرئياً. يفشل في نظام لا تتحكم به، وبإيقاع لا تراه، والتغذية الراجعة هي غياب تدريجي لزيارات صفحات لم تكن مصنَّفة أصلاً — وهو ما يبدو تماماً كموقع جديد لأنه جديد.
ونصف الـ hreflang أسوأ. خريطة اللغات الموروثة لا تعجز عن المساعدة فحسب، بل تطلق ادّعاءً خاطئاً موجباً: تخبر محرك البحث أن البديل العربي لصفحة /en/pricing هو الصفحة الرئيسية العربية. وhreflang خاطئ أسوأ من غيابه، لأن غيابه يترك المحرك يخمّن، أما الخاطئ فيوقف التخمين.
القاعدة التي نلتزم بها الآن: كل route قابل للفهرسة يستدعي alternates() داخل generateMetadata الخاص به. ليست "ينبغي" — فالـ route الذي لا يفعل عطبٌ، لأن وراثة الـ metadata تعني أن الإغفال ليس محايداً.
والنصف الثاني من الإصلاح أهم من الأول: الـ canonical وخريطة اللغات تبنيهما دالة واحدة، من المسار المجرَّد من بادئة الـ locale:
export function alternates(path: string, locale: Locale) {
const href = (l: Locale) => `/${l}${path === '/' ? '' : path}`;
const languages = Object.fromEntries(locales.map((l) => [l, href(l)]));
languages['x-default'] = href(defaultLocale);
return {canonical: href(locale), languages};
}كتابة الـ alternates يدوياً لكل page كانت ستصلح الصفحات الـ 24 وتضمن عودة العطب نفسه يوم يُضاف locale ثالث. أما اشتقاق كل صياغة للعنوان من مكان واحد فهو ما يجعلها عاجزة عن الاختلاف — بما في ذلك الـ trailing slash في رئيسية الـ locale، حيث /ar و/ar/ صياغتان لصفحة واحدة، والـ canonical موجود تحديداً ليختار إحداهما.
view source لصفحة عميقة — لا الرئيسية — واقرأ الـ canonical. فحص من ثلاثين ثانية، وهو الذي لا يجريه أحد.hreflang يشير إلى عنوان يُعيد 200 وهو نفسه canonical. وسلسلة من canonical إلى redirect إلى canonical إشارة مهدورة.x-default، ووجّهه إلى الـ locale الذي تخدم به بقية العالم فعلاً.hreflang متبادلاً. إن سمّت الصفحة الإنكليزية العربيةَ، فيجب أن تسمّي العربيةُ الإنكليزيةَ. الإشارات أحادية الاتجاه تُهمَل.route. نمط الفشل صامت بحكم بنيته، فلا بد أن يكون عادة لا اكتشافاً.الأمن
4 ثغرات في NestJS خلال 7 أشهر، وكلها ثغرة واحدة: الـ matcher والـ handler اختلفا على العنوان نفسه. مكان التحقق هو حيث تُقرأ البيانات.
الأمن
ثغرة RCE بدرجة 10.0، ثم 6 ترقيعات DoS في 8 أشهر. حين يكون العقد "اقبل كل ما يرمّزه الـ protocol"، فإغلاق ثغرة يترك البقية.
الأمن
18 إصداراً خبيثاً من package واحدة خرجت في ساعتين ونصف، تحصد الـ tokens عند التنصيب. lockfiles، وتعطيل الـ scripts، واعتمادات قصيرة العمر.