دودة npm تزرع hooks داخل الـ AI coding agent

في الرابع من أغسطس 2026، الساعة 09:35 UTC، نُشرت النسخة keyv@6.0.0 على npm ومعها payload خبيث. خلال أقل من ساعة لحقتها عائلة cacheable كاملة: flat-cache، file-entry-cache، cacheable-request، وغيرها. الحملة — امتداد لموجة Shai-Hulud — وصلت إلى أكثر من 400 اسم package عبر أكثر من ألف نسخة، بمجموع تنزيلات شهرية يتجاوز ملياري عملية تثبيت.

الجديد هنا ليس حجم الانتشار. الجديد أن الـ payload لم يكتفِ بسرقة الأذونات، بل ترك خلفه بابًا داخل أدوات المطوّر نفسها — بما فيها الـ AI coding agent.

المشكلة: الثقة تنتقل مع الشجرة

keyv ليست dependency تختارها عادةً بنفسك. تصل إليك عبر eslint، عبر build tools، عبر عشرات الطبقات التي لا تفتحها أبدًا. حين تُسمّم حزمة بهذا الموقع، الشجرة كلها تحتها تصبح ناقلًا.

الأسوأ: الـ provenance عملت كما هو مصمَّم لها. سجل npm يوثّق أن keyv@6.0.0 نُشرت عبر GitHub Actions كـ trusted publisher، ومعها attestations صالحة. لكن الكود الخبيث كان موجودًا في الـ repo لحظة بناء الـ artifact. التوقيع يثبت من أين جاء الملف، لا ما بداخله.

ما الذي تغيّر: persistence داخل أدوات المطوّر

الـ payload يعمل على مرحلتين. الأولى preinstall hook يشغّل setup.mjs عند التثبيت. الثانية تحمّل ملفًا باسم Math_Symbol.js يمسح الجهاز بحثًا عن GitHub وnpm tokens، مفاتيح السحابة، Vault tokens، Kubernetes service account tokens، وسلاسل الاتصال بـ قاعدة البيانات — ثم يستخدمها لإعادة النشر ذاتيًا.

المرحلة اللافتة هي الثالثة. الدودة تكتب داخل الـ repo ملفّين:

  • .claude/settings.json يحمل hook من نوع SessionStart.
  • .vscode/tasks.json يحمل task بخاصية "runOn": "folderOpen".

بمعنى آخر: حتى لو نظّفت الـ node_modules، يكفي أن يفتح أحد الفريق المجلد في المحرّر، أو يبدأ جلسة مع الـ agent، ليعود التنفيذ. هذه ليست ثغرة في الأدوات — VS Code يطلب workspace trust قبل تشغيل المهام التلقائية، والـ agent يطبّق المنطق نفسه على الإعدادات القادمة من الـ repo. لكن في مشروع يعمل عليه المطوّر يوميًا، تلك الموافقة أُعطيت من زمن.

فحص عملي

ابدأ بالبحث عن آثار الـ persistence، قبل أي إعادة تدوير للمفاتيح:

bash
# 1) آثار التنفيذ المزروعة داخل الـ repo
grep -rn "Math_Symbol" . --include="*.js" --include="*.mjs" 2>/dev/null
grep -n  "SessionStart" .claude/settings.json 2>/dev/null
grep -n  "folderOpen"   .vscode/tasks.json    2>/dev/null

# 2) هل تسحب شجرتك نسخة مصابة؟
npm ls keyv flat-cache file-entry-cache cacheable cacheable-request

ثم أغلق النسخ المصابة على مستوى الشجرة كلها عبر overrides في package.json:

json
{
  "overrides": {
    "keyv": "<6.0.0",
    "flat-cache": "<6.1.24",
    "file-entry-cache": "<11.1.6",
    "cacheable": "<2.5.1",
    "cacheable-request": "<13.0.20"
  }
}

تحفّظات من الواقع

الترتيب مهم. إعادة تدوير الـ tokens قبل إزالة الـ persistence تعني أنك سلّمت المهاجم المفاتيح الجديدة. نظّف أولًا، ثم دوّر.

سحب النسخة من الـ registry ليس فوريًا. التحذيرات العامة ظهرت حوالي 10:18 UTC، وبعد ساعة كانت ثمانية إصدارات ما زالت تحمل الـ tag المسمّى latest. أي build في تلك النافذة قد يكون سحب النسخة المسمومة.

`--ignore-scripts` وحده لا يكفي. هذه الحملة استخدمت preinstall، لكن موجات سابقة نقلت التنفيذ إلى import time — وهو ما غطّيناه سابقًا هنا. تعطيل الـ scripts خطوة، لا حل.

الـ lockfile هو خط الدفاع الحقيقي. npm ci مع تأخير زمني قبل تبنّي أي نسخة جديدة يقلّل نافذة التعرّض أكثر من أي فحص لاحق.

الخلاصة

سلسلة التوريد لم تعد تنتهي عند الـ registry. صارت تمتد إلى ملفات الإعداد التي يقرأها المحرّر والـ agent عند فتح المشروع. عامِل .claude/ و.vscode/ كما تعامل أي كود قابل للتنفيذ: راجعها في الـ code review، وثبّتها في الـ lockfile الذهني للفريق.

المصدر الأساسي: تحليل Snyk للحادثة وتقرير Socket.