تحصين سلسلة إمداد حزم npm في 2026: سكربتات دورة الحياة وإعداد minimumReleaseAge والـ postinstall الذي توقفنا عن الوثوق به
جدول المحتويات
عن الكاتب

Ahmed Mahmoud
كاتب ومطوّر
مهندس برمجيات شغوف بتطوير الويب وتصميم تجربة المستخدم.
مؤسّس ديفيا · eng-ahmed.com ↗الهجمات التي نجحت فعليًا على npm خلال العامين الماضيين نُفِّذت لحظة التثبيت، عبر سكربتات دورة الحياة، وخلال ساعات من نشر الإصدار الخبيث. في ديفيا نعتبر ثلاثة ضوابط هي الحد الأدنى في كل مستودع Node نطلقه: تعطيل سكربتات دورة الحياة للاعتمادات افتراضيًا، ورفض تثبيت أي إصدار عمره أقل من بضعة أيام، وعدم وجود أي رمز نشر طويل الأجل داخل خطوط التكامل المستمر. ولا يتطلب أي من هذه الضوابط تعديل سطر واحد من شيفرة التطبيق.
أهم النقاط
سكربتات دورة الحياة في npm، وهي preinstall وinstall وpostinstall وprepare، عبارة عن أوامر صدفة معرَّفة داخل ملف package.json الخاص بالاعتماد نفسه، ويشغّلها مدير الحزم بصلاحيات المطوّر أثناء تنفيذ npm install، أي قبل تشغيل أي شيفرة من التطبيق.
توقّف pnpm 10 عن تشغيل سكربتات دورة الحياة للاعتمادات افتراضيًا، وأي حزمة تحتاج فعلًا إلى خطوة بناء يجب إدراجها ضمن onlyBuiltDependencies. لا يوجد إعداد افتراضي مكافئ في npm، لذلك نضبط ignore-scripts=true في ملف .npmrc ونضع قائمة سماح صريحة.
الإعداد minimumReleaseAge هو فترة تهدئة زمنية، متاح في pnpm وفي Renovate وكذلك عبر إعداد cooldown في Dependabot، ويرفض أي إصدار نُشر قبل أقل من عدد محدد من الدقائق. تُكتشف الإصدارات المخترقة عادةً وتُسحب خلال ساعات إلى أيام قليلة، لذا تمتص فترة التهدئة النافذة الزمنية نفسها التي كان الفريق سيثبّت خلالها الإصدار الخبيث.
إثباتات المنشأ في npm تُثبت أي مستودع وأي التزام برمجي وأي مسار تكامل مستمر أنتج الحزمة، لكنها لا تُثبت أن الشيفرة سليمة، لأن مسار بناء مخترقًا يُصدر إثبات منشأ صالحًا تمامًا.
النشر الموثوق عبر OIDC يزيل رمز npm طويل الأجل من بيئة التكامل المستمر. وهذا الرمز تحديدًا هو ما جمعته الدودة ذاتية الانتشار على npm عام 2025 من سكربتات postinstall كي تنشر نفسها داخل بقية حزم المشرف المخترق.
ما الذي يُنفَّذ فعليًا عند كتابة npm install؟
الأمر npm install ينفّذ شيفرة عشوائية من شجرة الاعتمادات قبل تشغيل أي شيفرة يملكها الفريق. سكربت دورة الحياة أمر معرَّف في حقل scripts داخل الحزمة، يستدعيه مدير الحزم تلقائيًا لحظة التثبيت، ويرث بيئة الصدفة وملف ~/.npmrc ومفاتيح SSH وأي بيانات اعتماد سحابية موجودة على القرص. ولا توجد أي عزلة أمنية حوله افتراضيًا.
التفصيل الذي تقلّل معظم الفرق من وزنه هو أن هذا ينطبق على الشجرة الانتقالية بالكامل، لا على الاعتمادات المختارة فقط. خمسة اعتمادات مباشرة تتحول عادةً إلى مئات الحزم، وأي واحدة منها قادرة على تعريف سكربت postinstall لم يقرأه أحد في الفريق.
الخطوة العملية الأولى هي قياس عدد الحزم التي تطالب بهذا الحق داخل الشجرة. يوفّر npm محدِّد اعتمادات لهذا الغرض: تشغيل npm query بمحدِّد الحزم التي تعرّف الخاصية postinstall يسردها مباشرة. أما في pnpm 10 فالمكافئ هو الأمر pnpm approve-builds، الذي يسرد الحزم المحجوبة عن تشغيل سكربتات البناء ويسأل عن أي منها يُسمح له. في أول تشغيل لأي من الأمرين على مستودع ناضج، كانت القائمة أقصر مما توقعنا وأقل مشروعية مما تمنينا: حفنة من الوحدات الأصلية التي تُترجَم فعلًا، إضافة إلى حزم JavaScript خالصة تُرسل نداءات تتبّع تحليلية.
كيف نمنع تشغيل سكربتات دورة الحياة للاعتمادات؟
اضبط ignore-scripts=true في ملف .npmrc الخاص بالمشروع عند استخدام npm أو Yarn، أو ابقَ على السلوك الافتراضي في pnpm 10 الذي لا يشغّل سكربتات الاعتمادات، ثم ضع قائمة سماح للعدد القليل من الحزم التي تحتاج فعلًا إلى خطوة بناء.
التكلفة الصريحة أن إعداد ignore-scripts في npm غير انتقائي: فهو يعطّل أيضًا سكربتات المشروع نفسه، ما يعني أن مستودعًا يعتمد على prepare لتثبيت خطافات Husky سيتوقف عن ذلك بصمت. نعالج هذا بنقل أعمال التهيئة إلى خطوة صريحة باسم npm run setup يستدعيها ملف README ووظيفة التكامل المستمر معًا، وبتشغيل npm rebuild للوحدات الأصلية مثل sharp وbetter-sqlite3 التي تحتاج إلى ترجمة.
نسخة pnpm من الضابط نفسه أضيق وأفضل، لأن قائمة السماح تكون لكل حزمة على حدة بدلًا من الكل أو لا شيء: إضافة مصفوفة onlyBuiltDependencies تحت المفتاح pnpm في package.json تسمح تحديدًا للحزم المذكورة فيها. القاعدة التي نتبعها لتحديد ما يستحق الإدراج: الإضافات الأصلية ومنزّلات الملفات التنفيذية مثل esbuild وplaywright لديها سبب حقيقي للعمل لحظة التثبيت، أما أداة JavaScript خالصة تطلب سكربت postinstall فلا، وهذا الطلب نفسه هو المؤشر الذي يستحق التدقيق.
ما هو minimumReleaseAge ولماذا توقف فترة التهدئة الهجمات ذات الطابع الدودي؟
الإعداد minimumReleaseAge هو إعداد في مدير الحزم يرفض تثبيت أي إصدار نُشر قبل أقل من عدد محدد من الدقائق. يوفّره pnpm كإعداد أساسي يُقرأ من ملف pnpm-workspace.yaml، ويدعمه Renovate منذ سنوات كخيار إعداد، ويقدّم Dependabot الفكرة نفسها عبر كتلة cooldown في ملف dependabot.yml. نشغّل لدينا فترة تهدئة مدتها ثلاثة أيام مع استثناء لحزمنا ذات النطاق الخاص، لأنها تُنشر وتُنشر إلى الإنتاج في اليوم نفسه.
فترة التهدئة فعّالة بشكل غير متناسب لأن عمر الإصدار الخبيث على npm قصير. يكتشفه مشرفو السجل وأدوات الفحص وبقية المطورين خلال ساعات إلى يومين، ثم يُسحب الإصدار أو يُوسم بأنه مهمل. التهدئة لا تجعل الفريق أذكى من المهاجم، بل تجعله متأخرًا عمّن يكتشف الهجوم، وهي نتيجة مكافئة بجزء بسيط من الجهد.
المقايضة حقيقية وتستحق الإفصاح: الفريق الذي يشغّل فترة تهدئة يتأخر عمدًا ثلاثة أيام عن كل شيء، بما في ذلك تصحيحات الأمان. لذلك نحتفظ بمسار تجاوز موثّق، وهو استثناء من سطر واحد في الإعداد مع طلب دمج يشرح السبب، حتى يبقى سحب تصحيح ثغرة عاجل في اليوم نفسه ممكنًا ومرئيًا في المراجعة.
هل يثبت إثبات المنشأ في npm أن الحزمة آمنة؟
لا. إثبات المنشأ في npm شهادة موقّعة تُولَّد عبر npm publish مع الراية --provenance داخل مزوّد تكامل مستمر مدعوم، وتربط الحزمة المنشورة بمستودع المصدر والالتزام البرمجي ومسار العمل الذي بناها. هي تثبت المصدر لا النية. ويتحقق الأمر npm audit signatures من تواقيع السجل والإثباتات عبر الشجرة المثبتة.
ما يوفره إثبات المنشأ عمليًا: رمز مسروق من حاسوب مشرف لم يعد قادرًا على نشر حزمة تدّعي أنها من مستودع معيّن وتحمل إثباتًا صالحًا، كما يمكن مقارنة الحزمة بالالتزام البرمجي الذي تدّعيه. وما لا يغطيه: مشرف يدمج التزامًا خبيثًا، أو مسار بناء مخترق، أو مئات الحزم الانتقالية التي لا تنشر أي إثبات أصلًا.
الضابط المكمّل هو النشر الموثوق، حيث يستبدل npm رمزًا قصير الأجل صادرًا عن تشغيلة التكامل المستمر عبر OIDC بدلًا من قراءة سر طويل الأجل. وهذا مهم تحديدًا لأن سرقة الرموز هي ما حوّل دودة npm عام 2025 من حادثة إلى انتشار: فحمولتها في سكربت postinstall كانت تجمع بيانات الاعتماد وتستخدمها لنشر نفسها في بقية ما يملكه المشرف المخترق. ومسار نشر لا يحتوي على رمز طويل الأجل في بيئته لا يترك شيئًا يُسرق.
ما الذي يجب تغييره في التكامل المستمر لتقليل نطاق الضرر؟
افترض أن إحدى عمليات التثبيت ستنفّذ شيفرة معادية يومًا ما، واجعل تلك العملية هدفًا عديم القيمة. أربعة تغييرات تنجز معظم العمل.
استخدم npm ci بدل npm install داخل التكامل المستمر. الأمر npm ci يثبّت حصريًا من ملف package-lock.json ويفشل عند اختلاف ملف القفل عن package.json، ما يلغي الانزياح الصامت في إصدارات الاعتمادات الانتقالية بين ما راجعه إنسان وما حلّه خط التكامل المستمر.
ثبّت إجراءات GitHub Actions على بصمة التزام كاملة من أربعين محرفًا بدلًا من الوسم. وسوم Git قابلة للتعديل، وقد عمل اختراق tj-actions/changed-files عام 2025 عبر إعادة توجيه وسوم قائمة إلى التزام خبيث، فالتقطته تلقائيًا كل المسارات المثبتة على وسم الإصدار الرئيسي.
أبقِ الأسرار خارج الوظيفة التي تثبّت الاعتمادات. خطوة التثبيت لا تحتاج رمز نشر ولا مفتاح سحابة ولا بيانات اعتماد نشر. وفصل التثبيت والبناء عن النشر والإطلاق في وظائف منفصلة، مع صلاحيات قراءة فقط على المحتوى في الأولى، يعني أن أي حمولة تُنفَّذ لحظة التثبيت ستعمل داخل وظيفة لا تحوي شيئًا يستحق السرقة.
احفظ ملف القفل في المستودع وراجع فروقه. بصمة integrity تثبّت البايتات بدقة، ما يجعل فرق ملف القفل المكان الوحيد الذي تظهر فيه قفزة انتقالية مفاجئة أمام المراجع فعليًا.
أي خطوة تحصين تعطي أكبر أمان مقابل أقل ألم؟
تعطيل سكربتات دورة الحياة عبر ignore-scripts أو onlyBuiltDependencies يوقف تنفيذ الشيفرة لحظة التثبيت من أي حزمة في الشجرة، مقابل تعطّل الوحدات الأصلية حتى تُدرج في قائمة السماح.
فترة التهدئة minimumReleaseAge توقف الإصدارات المخترقة حديثة النشر، وهي الشكل الغالب للهجوم، مقابل التأخر عدة أيام عن كل التحديثات بما فيها تصحيحات الأمان.
استخدام npm ci مع ملف قفل محفوظ في المستودع يوقف انزياح الإصدارات الانتقالية غير المراجَع بين المراجعة والنشر، وتكلفته معدومة عمليًا.
تثبيت إجراءات GitHub على بصمة التزام يوقف اختطاف المسار عبر الوسوم القابلة للتعديل، مقابل عبء تحديثات يمكن أتمتته عبر Renovate.
النشر الموثوق عبر OIDC يوقف سرقة الرموز وبالتالي الانتشار الذاتي ذا الطابع الدودي، مقابل إعداد لمرة واحدة واشتراط أن يتم النشر من التكامل المستمر.
إثبات المنشأ مع npm audit signatures يوقف الحزم التي لم تُبنَ من المصدر الذي تدّعيه، لكنه لا يوقف مصدرًا خبيثًا، وتغطيته في المنظومة ما تزال جزئية.
لو كان علينا إطلاق ضابط واحد فقط لفريق غدًا لاخترنا فترة التهدئة، لأنها لا تتطلب صيانة قائمة سماح ولا تغييرًا في سلوك المطورين. تعطيل سكربتات دورة الحياة أقوى منها بوضوح، لكنه يحمل تكلفة في الأسبوع الأول ريثما يكتشف الفريق أي الحزم كانت تحتاجها فعلًا.
الأسئلة الشائعة
س: هل يكسر الإعداد ignore-scripts=true المشروع؟
ج: قد يفعل، لأن إعداد ignore-scripts في npm يعطّل أيضًا سكربتات دورة الحياة الخاصة بالمشروع مثل prepare، وهي الطريقة التي يثبّت بها Husky خطافات Git. انقل ذلك العمل إلى خطوة صريحة باسم npm run setup وشغّل npm rebuild للوحدات الأصلية التي تحتاج ترجمة.
س: هل يحجب pnpm سكربتات التثبيت افتراضيًا؟
ج: نعم. لا يشغّل pnpm 10 سكربتات دورة الحياة للاعتمادات ما لم تكن الحزمة مدرجة في onlyBuiltDependencies، ويعرض الأمر pnpm approve-builds الحزم المحجوبة كي يُسمح لها فرديًا.
س: كم يجب أن تكون مدة فترة التهدئة؟
ج: من ثلاثة إلى سبعة أيام تغطي نافذة الاكتشاف لمعظم اختراقات npm المعلنة. احتفظ بمسار تجاوز موثّق كي يبقى سحب تصحيح أمني عاجل ممكنًا في اليوم نفسه.
س: ألا يغطي npm audit هذا الأمر أصلًا؟
ج: لا. يبلّغ npm audit عن ثغرات معروفة من نشرات أمنية منشورة، والإصدار الخبيث لا توجد له نشرة في اللحظة التي سيثبّته فيها الفريق. والأمر npm audit signatures فحص مختلف يتحقق من تواقيع السجل وإثباتات المنشأ، والاثنان متكاملان لا بديلان.
س: هل تحمي بصمة integrity في ملف القفل من حزمة خبيثة؟
ج: حقل integrity بصمة موارد فرعية تضمن استلام البايتات نفسها التي كانت وقت كتابة ملف القفل. وهو يحمي من العبث بين السجل والجهاز، لكنه لا يقول شيئًا عن كون تلك البايتات خبيثة وقت نشرها.
قراءات إضافية
هندسة الواجهات
الوضع الداكن دون وميض في Next.js 16: كيف نستخدم سكربت سمة حاجبًا وcolor-scheme وزر تبديل آمنًا مع الـ Hydration
وميض الوضع الداكن عند إعادة تحميل الصفحة مشكلة توقيت: الخادم لا يستطيع قراءة localStorage. نشرح سكربت السمة الحاجب الذي نستخدمه في Next.js 16، ولماذا لا نضع السمة في cookie حفاظًا على الرسم الثابت، وكيف نبني زر تبديل يمر بالـ hydration دون أي عدم تطابق.
هندسة الواجهات
صور Open Graph الديناميكية في Next.js 16: كيف نبني opengraph-image.tsx ونتحقق من إصدار Satori الذي تضمّنه next/og ونحافظ على صحة معاينات الروابط
واجهت بطاقات المشاركة المولّدة لدينا ثلاث مفاجآت: خط WOFF2 لم يستطع المحرّك قراءته، وملف README لـ Satori يصف محرّكًا أحدث من المضمّن في Next.js، وصفحة ظلّت تعرض صورة الـ layout. هذه ملاحظاتنا الميدانية حول opengraph-image.tsx في Next.js 16 وأولوية البيانات الوصفية وفحص og:image قبل مشاركة أي رابط.