الشيفرة كانت سليمة، والناشر لم يكن كذلك
سجلّان في المستودع، بينهما إصدار واحد. نشر 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 12، المتاح للجميع منذ 8 يوليو 2026، لم يعد يشغّل سكربتات preinstall وinstall وpostinstall الخاصة بالاعتماديات ما لم يسمح بها المشروع، وتُسجَّل التصاريح في قائمة allowScripts التي تُدرجونها في المستودع. [7][8] وعلى npm 12 ما كان خطّاف plain-crypto-js ليعمل دون إذن صريح. لكن افحصوا ما تستخدمه بيئة CI لديكم فعلًا: صورة node:24-alpine تحمل في 7 أكتوبر 2026 الإصدار npm 11.19.0، وما زال npm 11 ينفّذ سكربتات الاعتماديات افتراضيًا. وتحظرها pnpm افتراضيًا منذ الإصدار 10، وحتى هذا الإعداد احتاج إلى تحقق: قبل الإصدار 10.26.0 كان بوسع الاعتماديات المستضافة على Git تنفيذ شيفرة أثناء التثبيت رغم ذلك، وهي ثغرة عالية الخطورة. [9][10] يصير الإعداد حدًّا أمنيًا حين تراه يمنع شيئًا بالفعل.
وللحلقة الرابعة إجراء رخيص أيضًا. أضاف npm 11.10.0 في فبراير 2026 الإعداد min-release-age، وهو مهلة انتظار بالأيام. وفي pnpm يُحدَّد minimumReleaseAge بالدقائق، وقيمته الافتراضية في pnpm 11 يوم واحد. [10][11] عاشت إصدارات axios المسمومة أقل من ثلاث ساعات، فحتى مهلة يوم واحد تتجاوزها. المهلة رهان على أن يلاحظ أحدهم التهديد خلال النافذة، ولا تنفع ضد مهاجم صبور. أضيفوا إليها trustPolicy: no-downgrade في pnpm، الذي يرفض إصدارًا انخفض مستوى الثقة فيه مقارنة بالإصدارات السابقة. [10] وهو مصمم تمامًا لصورة 1.14.1، لكنني لم أجرّب هذا الهجوم ضده لأن 1.14.1 لم يعد موجودًا في المستودع. اختبروه على حزمة تتحكمون بها.
link control owner
1 maintainer laptop no publish credential on a workstation publisher
2 registry gate trusted publisher + "disallow tokens" publisher
3 decoy dependency diff the manifest; question new deps in patches you
4 fresh resolution npm ci + lockfile; min-release-age / pnpm cooldown you
5 install script npm 12 allowScripts; pnpm 10+; --ignore-scripts you
6 reachable secrets short-lived creds, split jobs, egress allow-list you
+ trust downgrade pnpm trustPolicy: no-downgrade youثمانية فحوصات قبل أمر 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المصادر
- [1] مشرفو Axios: تقرير مراجعة الحادث
- [2] StepSecurity: اختراق axios على npm، تحليل تقني
- [3] Google Threat Intelligence: فاعل مرتبط بكوريا الشمالية يستهدف axios
- [4] Microsoft: إجراءات التخفيف من اختراق axios على npm
- [5] JFrog Security Research: تحليل البرمجية الخبيثة plain-crypto-js
- [6] وثائق npm: الناشرون الموثوقون
- [7] سجل تغييرات GitHub: أمان npm وقت التثبيت وإيقاف رموز تجاوز المصادقة الثنائية
- [8] وثائق npm: سكربتات التثبيت في npm 12
- [9] نشرة أمان pnpm GHSA-379q-355j-w6rj: تجاوز حظر سكربتات دورة الحياة
- [10] وثائق pnpm: إعدادات أمان سلسلة التوريد
- [11] إصدار npm CLI v11.10.0: min-release-age
- [12] OpenAI: اختراق أداة التطوير axios
هل تحتاج إلى هذا الفحص في منتجك؟
مراجعة أمان الشيفرة