المدونة / أخبار أمنية

اختراق axios على npm: كيف انكسرت سلسلة الثقة

لم يكن اختراق axios على npm ثغرة في الشيفرة. نتتبع حلقات سلسلة الثقة الست، والإجراء الذي يكسر كل حلقة، وما الذي يغيّره npm 12.

أكملت دراسة الدكتوراه في أمن المعلوماتاجتزت المناقشة الجامعية · المناقشة النهائية للأطروحة لم تُجرَ بعد

الشيفرة كانت سليمة، والناشر لم يكن كذلك

سجلّان في المستودع، بينهما إصدار واحد. نشر GitHub Actions الإصدار axios 1.14.0 عبر ناشر موثوق، ويحمل شهادة منشأ (provenance). أما axios 1.14.1، الذي ظهر في 00:21 بتوقيت UTC يوم 31 مارس 2026، فنُشر يدويًا دون أيٍّ من ذلك. هذا الفارق هو قصة اختراق axios على npm كاملة، وكان موجودًا في بيانات المستودع الوصفية. [2]

الوقائع باختصار. أضاف الإصداران axios 1.14.1 و0.30.4، اللذان نُشرا بفارق 39 دقيقة، اعتمادية جديدة واحدة هي plain-crypto-js@4.2.1، أنزل سكربت postinstall فيها حصان طروادة للتحكم عن بُعد على أي جهاز نفّذ التثبيت. وبحلول 03:15 UTC أزالت npm الإصدارين. [1][2] وتنسب Google العملية بثقة عالية إلى UNC1069، وهي مجموعة مرتبطة بكوريا الشمالية، بينما تتتبع Microsoft الفاعل باسم Sapphire Sleet. [3][4]

أسمّي هذا هجومًا على سلسلة الثقة. يغيّر هجوم سلسلة توريد البرمجيات الشيفرة التي تشغّلها دون أن يغيّر سطرًا واحدًا مما راجعتَه، عبر اختراق قناة توصيل الاعتمادية. لم تُستغل ثغرة في مصدر axios. كل حلقة هنا قرار بالثقة اتخذه أحدهم، وهذه أنفع طريقة لقراءة الحادثة. لم أُعد إنتاج شيء من هذا بنفسي؛ التحليل مبني على تقرير المشرفين على المشروع وتقارير الشركات وسجلات المستودع حتى 7 أكتوبر 2026.

axios@1.14.0   published by CI
  _npmUser           GitHub Actions
  trustedPublisher   github
  gitHead            46bee3dea75ef53a8eae49f3b7487e6341de6074
  dist.attestations  SLSA provenance v1

axios@1.14.1   2026-03-31T00:21:58Z, published by hand
  _npmUser           the lead maintainer's account
  trustedPublisher   absent
  gitHead            absent
  GitHub commit/tag  none; the release exists only on npm

بدأت السلسلة على حاسوب محمول وانتهت على حاسوب محمول

لم تكن الحلقة الأولى npm ولا GitHub. وفق تقرير المشرفين، وصل المهاجم إلى حاسوب كبير المشرفين عبر حملة هندسة اجتماعية موجهة وبرمجية RAT خبيثة، وأعطاه ذلك بيانات اعتماد npm التي نُشرت بها الإصدارات. [1] أنتج RAT على جهاز مطوّر الرمز المميز (token). وأنتج الرمز RAT على كل جهاز مطوّر وكل مشغّل CI ثبّت الإصدار المسموم. الفئة نفسها من البرمجيات الخبيثة عند الطرفين.

كان المشرفون صريحين: النشر مباشرة من حساب شخصي كان خطرًا يمكن تجنبه، وكان ينبغي أن يكون مسار OIDC والإصدارات غير القابلة للتغيير، اللذان يعتمدونهما الآن، قائمين قبل وقوع ما وقع. [1] تعجبني هذه الجملة. فهي تسمّي الدرس الحقيقي، وهو لا يخص axios، بل المكان الذي تعيش فيه صلاحية النشر.

أقرأ الحادثة على أنها ست حلقات، ترونها في المخطط أدناه. في كل حلقة كان يمكن لإجراء حماية أن يكسر السلسلة، ولكل حلقة مالك مختلف. بعضها للناشر. وأغلبها لكم. سأمرّ عليها بالترتيب.

1  maintainer laptop    social engineering + RAT         -> npm credentials
2  registry gate        direct token publish accepted    -> axios 1.14.1, 0.30.4
3  decoy dependency     plain-crypto-js 4.2.0 -> 4.2.1   -> new line in package.json
4  your resolver        fresh install, no cooldown       -> poisoned tarball fetched
5  install script       postinstall: node setup.js       -> dropper runs
6  runner or laptop     reachable secrets, open egress   -> RAT, then whatever it can reach

لماذا لم يوقف النشر الموثوق عبر OIDC رمزًا مسروقًا؟

لأنه لم يكن الباب الوحيد. يسمح النشر الموثوق (trusted publishing) للمستودع بقبول النشر من سير عمل CI مسمّى فقط، باعتماد OIDC قصير العمر بدل سرّ مخزَّن. وكانت إصدارات axios 1.x تمر بهذا المسار أصلًا، ولذلك يُظهر 1.14.0 ناشرًا موثوقًا وشهادة منشأ. [2][6] أما 1.14.1 الخبيث فنُشر مباشرة من حساب، دون ناشر موثوق ودون gitHead ودون التزام (commit) أو وسم (tag) مطابق في المستودع. [2]

وجود مسار نشر جيد لا يلغي المسار السيئ. ما دامت الحزمة تقبل الرموز المميزة، فمن يملك رمزًا يتجاوز سير العمل كله. والذي يغلق هذا الباب إعداد على مستوى الحزمة: Require two-factor authentication and disallow tokens، وتوصي به وثائق npm بعد ضبط الناشرين الموثوقين، وتبقى الناشرون الموثوقون تعمل كالمعتاد. [6] تقرأ StepSecurity غياب البيانات الوصفية على أنه رمز كلاسيكي طويل العمر مسروق؛ بينما يقول المشرفون فقط إن المهاجم حصل على بيانات اعتماد الحساب. [1][2] وفي الحالتين لم يمرّ النشر بسير العمل.

رأيي: تفعيل OIDC دون تعطيل الرموز نصف إجراء، وكثير من الفرق تضع علامة الإنجاز بعد النصف الأول. إن كنتم تنشرون حزمًا، فافحصوا النصفين اليوم. وإن كنتم تستهلكونها، فالبيانات الوصفية في المستودع إشارة يمكنكم قراءتها: إصدار كان يُنشر عبر ناشر موثوق ثم لم يعد كذلك يستحق التوقف. وتشدد npm من الجهة الأخرى أيضًا: اعتبارًا من يناير 2027 تفقد الرموز الدقيقة (granular tokens) التي تتجاوز المصادقة الثنائية القدرة على النشر المباشر. [7]

اعتمادية لا يستوردها أحد هي نتيجة فحص بحد ذاتها

مهّد المهاجم الأرض قبل يوم. ظهرت plain-crypto-js@4.2.0 في 05:57 UTC يوم 30 مارس كطُعم نظيف: نسخة من شيفرة crypto-js الحقيقية دون خطّاف تثبيت. كانت مهمتها الوحيدة منح الحزمة تاريخ نشر كي لا تبدو جديدة تمامًا. وتبعها الإصدار الخبيث 4.2.1 في 23:59 UTC، قبل axios 1.14.1 بـ22 دقيقة. [2]

والآن الدليل. فحصت StepSecurity جميع الملفات الـ86 في axios@1.14.1 فوجدت أن plain-crypto-js لا تُستورد ولا تُستدعى بـrequire في أي مكان. [2] لم تكن للشيفرة التي شحنتها axios حاجة بهذه الاعتمادية. كما أُسقط من الإصدار سكربت prepare الخاص بـhusky، وهذا يتسق مع نشر يدوي تجاوز أدوات الإصدار المعتادة. [2] ببساطة: إصدار تصحيحي أضاف اعتمادية وقت تشغيل لا تستخدمها الحزمة. عند سطر كهذا يتوقف المراجع البشري.

هذا الفحص هو أول ما كنت سأؤتمته. عند كل ترقية، قارنوا ملف الوصف (manifest) بين الإصدار القديم والجديد. اعتمادية جديدة في قفزة تصحيحية تذهب إلى إنسان. ثم ثلاثة أسئلة: ما عمر الحزمة، ومن يصونها، وهل تستخدمها الحزمة الأم فعلًا؟ تجيب الأوامر أدناه عن الثلاثة في أقل من دقيقة. وبوت التحديثات لديكم لن يطرح هذه الأسئلة عنكم.

# what changed in the manifest between two versions
npm diff --diff=<pkg>@<old> --diff=<pkg>@<new> package.json

# does the package actually use the new dependency?
npm pack <pkg>@<new> && tar -xzf <pkg>-<new>.tgz && grep -R "<new-dep>" package/

# how old is the new dependency, and who maintains it?
npm view <new-dep> time maintainers --json

لماذا كفى أمر npm install لتسليم الجهاز؟

لأن npm ينفّذ شيفرة أثناء التثبيت. سكربت دورة الحياة (lifecycle script) أمر تعلنه الحزمة في package.json ويشغّله مدير الحزم تلقائيًا، وقد أعلنت plain-crypto-js@4.2.1 الأمر postinstall: node setup.js. [3] ويعمل قبل أن يستورد تطبيقكم أي شيء، ولذلك تعرّض حتى المشروع الذي لم يلمس المكتبة وقت التشغيل. رصدت StepSecurity أول اتصال بخادم المهاجم خلال ثانيتين من npm install. [2]

التعمية في setup.js أرقّ مما تبدو. تُعكس السلاسل النصية ثم تُفك من Base64 ثم يُجرى عليها XOR بالمفتاح OrDeR_7077 والثابت 333. [5] ويُظهر فك التعمية الذي أجرته StepSecurity أن المفتاح يمر عبر Number(): فتتحول الحروف إلى NaN، والـXOR مع الصفر لا يغيّر شيئًا، فيكون المفتاح الفعلي 0,0,0,0,0,0,7,0,7,7. [2] هذه حيلة للإفلات من ماسحات النصوص، وليست تشفيرًا. الدرس: التحليل الساكن لسكربتات التثبيت جرس إنذار مفيد، لكنه حدّ ضعيف.

بعد ذلك يتفرع المُنزِّل (dropper) بحسب المنصة ويطلب من خادم المهاجم مرحلة ثانية. يخبر جسم طلب POST الخادم أيّ مرحلة يرسل: product0 لنظام macOS، وproduct1 لنظام Windows، وproduct2 لنظام Linux. [2] ويبيّن الجدول أين تحط كل مرحلة. وترسل المتغيرات الثلاثة إشارة نبض كل 60 ثانية، بصيغة JSON مرمّزة بـBase64، وبوكيل مستخدم (User-Agent) لـInternet Explorer 8 على Windows XP. [3] ولأن RAT يُنزَّل لحظة الحاجة، لا توجد برمجية خبيثة بالمعنى الدقيق داخل الحزمة المضغوطة: الحزمة أداة تنزيل. لذلك تصبح حركة الخروج هي الحاسمة، ووكيل مستخدم IE8 صادر من مشغّل بناء هو أرخص تنبيه ستكتبونه. [3]

platform  stage two lands in                   started by
win32     %TEMP%\6202033.ps1                   VBScript; powershell.exe copied to %PROGRAMDATA%\wt.exe
darwin    /Library/Caches/com.apple.act.mond   Mach-O binary, chmod +x, zsh in the background
linux     /tmp/ld.py                           nohup python3

all       POST http://sfrclak[.]com:8000/6202033
          body: packages.npm.org/product0 (macOS), product1 (Windows), product2 (Linux)
          beacon: Base64 JSON every 60 seconds
          User-Agent: mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
          win32 persistence: HKCU Run key "MicrosoftUpdate" and %PROGRAMDATA%\system.bat

لماذا بدا المجلد نظيفًا بعد الهجوم؟

لأن المُنزِّل نظّف وراءه. بعد التنفيذ حذف setup.js نفسه وحذف ملف package.json الخبيث، ثم أعاد تسمية ملف package.md المجهَّز مسبقًا إلى package.json. [2] يعلن ملف الوصف البديل الإصدار 4.2.0 دون خطّاف تثبيت. فمن يفتح node_modules/plain-crypto-js لاحقًا يرى 4.2.0 البريئة، مع أن 4.2.1 هي التي عملت. [2] وهكذا يؤتي الطُّعم المزروع قبل يوم ثمره مرة ثانية.

لهذا أقول للفرق: افحصوا ملف القفل (lockfile) لا node_modules. وجود plain-crypto-js في ملف القفل، بالإصدار 4.2.0 أو 4.2.1، يعني أن رسم التثبيت لديكم ضمّ الهجوم، وبحسب توصية Google يُعدّ الجهاز مخترقًا من تلك اللحظة. [3] وما ينبغي حفظه وكيف يُثبت التعافي موجود في مقالي السابق عن Axios وأدلة البناء. أما هنا فأبقى عند الآلية.

ماذا فعل RAT؟ يسمّي تحليل Google الحمولة WAVESHAPER.V2؛ وهي تجمع معلومات النظام وتعرض محتويات المجلدات وتنفّذ أوامر عشوائية وتحقن ملفات تنفيذية. [3] هذا وصول، لا سرقة. وما يفعله المشغّل بالوصول إلى حاسوب مطوّر أو مشغّل CI هو قراءة الرموز والمفاتيح وملفات .env الموجودة هناك؛ وتُظهر حالة OpenAI حجم الرهان: سير عمل GitHub Actions في عملية توقيع تطبيقات macOS شغّل axios 1.14.1 وفي متناوله شهادة التوقيع ومواد التوثيق (notarization). لم تجد OpenAI دليلًا على الوصول إلى بيانات المستخدمين أو تعديل برمجياتها، وبدّلت الشهادة احترازًا. [12]

من الذي ينبغي أن يقلق فعلًا؟

جزء أصغر وأكثر تحديدًا من بيئتكم مما أوحت به العناوين. التطبيق المُسلَّم ليس الضحية. لا شيء يستورد plain-crypto-js، لذلك لم يكن لحزمة واجهة أمامية بُنيت من axios 1.14.1 سبب لتضمّن الحمولة؛ الضحية هي الجهاز الذي نفّذ التثبيت. [2] وكانت النافذة قصيرة أيضًا: من 00:21 UTC حتى الإزالة في 03:15 UTC، أقل من ثلاث ساعات. [1]

لكي تتأثروا كان لا بد من تحقق ثلاثة أمور. أن يحلّ تثبيت جديد axios 1.14.1 أو 0.30.4 في هذه النافذة، أي تثبيت دون ملف قفل مطابق أو بوت تحديثات دمج الطلب خلال ساعات. وأن يُسمح لسكربتات التثبيت بالعمل، وهو الإعداد الافتراضي. وأن يحتفظ الجهاز بشيء يستحق الوصول إليه. وخلال الحادثة نصحت Microsoft بتثبيت الإصدارات بدقة وإيقاف بوتات الاعتماديات الآلية. [4] إن استطعتم الإجابة عن هذه الأسئلة الثلاثة لكل مشغّل وكل حاسوب، فأنتم تعرفون تعرّضكم. وإن لم تستطيعوا، فهذه هي النتيجة الحقيقية.

ثمانية فحوصات قبل أمر npm install التالي

هذه هي النسخة من المقال التي يمكنكم العمل بها. ابدؤوا بمستودعاتكم، ثم صور CI، ثم الحزم التي تنشرونها. القائمة أدناه مرتبة بحسب سرعة العائد.

حين أراجع قاعدة شيفرة، يدخل ملف وصف الاعتماديات وملف القفل وإعدادات الإصدار في نطاق العمل، لأن حدود الثقة المهمة كثيرًا ما تكون هناك لا في الشيفرة التي كتبتموها. وهذا الجزء من السلسلة لا يصل إليه اختبار الاختراق المعتاد عادةً. إن أردتم عين المهاجم على كيفية ثقة منتجكم باعتمادياته ومسار إصداره، فأخبروني بما تسلّمونه. وإن ظننتم أنكم تأثرتم، فاحفظوا الأدلة قبل إعادة البناء واستعينوا بمختص في الاستجابة للحوادث: فهذا عمل مختلف عن عملي.

1  lockfile    grep -nE "plain-crypto-js" package-lock.json     # 4.2.0 or 4.2.1 means act
2  CI install  npm ci, never npm install; no bot merges within hours of a release
3  scripts     npm 12: review allowScripts | older npm: --ignore-scripts, allow by name | pnpm 10+: allowBuilds
4  cooldown    npm config set min-release-age 3 | pnpm: minimumReleaseAge
5  trust       pnpm: trustPolicy: no-downgrade
6  publishing  your packages: trusted publisher on, "disallow tokens" on, no publish token on a laptop
7  egress      build runners: default-deny outbound; alert on node spawning curl, powershell or python3
8  secrets     the install job holds no deploy, signing or cloud credentials

المصادر

هل تحتاج إلى هذا الفحص في منتجك؟

مراجعة أمان الشيفرة
جميع المقالات

08تواصل

هل تطلق منتجًا يتعامل مع بيانات المستخدمين؟

أرسل لي وصفًا مختصرًا. أرد خلال ثلاثة أيام بأسئلة أو بمخطط للعرض، ونتفق على السعر كتابيًا قبل بدء العمل.