هجوم سلسلة توريد PyPI: درس LiteLLM
اختراق LiteLLM على PyPI زرع برنامج سرقة بيانات في كل ملف wheel. ماذا كان سيوقف تثبيت الهاشات وملفات القفل، وماذا لا؟

ثلاثة ملفات. هذه هي البصمة الكاملة لواحدة من أخطر هجمات سلسلة توريد بايثون التي شهدناها: ملف cook.pth بحجم 12 بايت، وcook.py بحجم 22 كيلوبايت، وsitecustomize.py بحجم 25 كيلوبايت. عندما نُشر LiteLLM 1.107.0 على PyPI، رافقت هذه الملفات الثلاثة كل ملف wheel من ملفاته السبعة، وبسبب طريقة عمل آلية بدء تشغيل بايثون، كانت تُنفَّذ في كل عملية بايثون على الجهاز المصاب: تطبيقك، وبيئة CI الخاصة بك، وpip نفسه، حتى خادم اللغة في محرِّرك. لم تكن بحاجة لاستيراد litellm، ولا لتشغيله. كان يكفي تشغيل أي مفسِّر بايثون.
أكّد فريق LiteLLM الاختراق، وسحب PyPI الإصدار خلال ساعات قليلة، لكن النافذة كانت كافية. صور Docker الخبيثة، بما فيها وسم latest، ظلت متاحة نحو ساعتين ونصف. إن سحب نظام CI الخاص بك أثناء تلك الفترة، فقد كان البرنامج الخبيث قد جمع مفاتيح SSH وبيانات اعتماد السحابة وكلمات مرور المتصفح وأسرار CI/CD ورموز أدوات الذكاء الاصطناعي، ثم شفّرها وأرسلها إلى نطاق يسيطر عليه المهاجم. لم تكن هذه حزمة مزيفة باسم قريب أو تعتمد على الخطأ الإملائي في الاسم. كانت الحزمة الحقيقية، بنتها خط الإنتاج الحقيقي، ووقّعتها عملية الإصدار الحقيقية. وهذا يجب أن يغيّر طريقة تفكيرك في الثقة بالتبعيات.
كيف أختطفت ثلاثة ملفات خط إصدار الحزمة الرسمي
لم يخترق المهاجم PyPI، ولم يُدخل التزاماً خفياً إلى المستودع العام، فمصدر GitHub الأعلى نظيف. وبحسب تقرير الحادث الذي نشره LiteLLM، امتلك المهاجم بيانات اعتماد GitHub CLI لثلاثة مطورين، يبدو أنها استُخرجت من سجلات نشر داخلي لـ LiteLLM كان مكشوفاً على الإنترنت. وباستخدام رمز مالك المنظمة، تجاوز المهاجم حماية الفرع، ووجّه سير عمل الإصدار الخاص نحو مستودع شبيه باسم Berri-AI/litellm بدلاً من BerriAI/litellm. ثم استوفت بيانات اعتماد مطور ثانٍ شرط الموافقة. وبعدها بنى خط الإنتاج الرسمي أدوات المهاجم ووقّعها ونشرها.
كان الحقن نفسه صغيراً وأنيقاً. سطر إضافي واحد في pyproject.toml الخاص بملف sdist:
[tool.maturin]
python-source = "."
data = "cook" # <-- injected, copies cook/ into the wheel
python-packages = ["litellm_cli"] # legitimate, present upstream
يأمر التوجيه data = "cook" أداة maturin بتضمين مجلد cook/ داخل المسار purelib الخاص بملف wheel، فيثبّته pip في site-packages دون تردد. ملف sdist نفسه لم يتضمن المجلد، فالمراجع البشري الذي يتصفح الإصدار لن يرى سوى سطر واحد غامض ولا شيء غيره. اختبأت الحمولة في ناتج البناء، لا في الشيفرة المصدرية. ولو راجعت تاريخ git، وهو ما يعتبره معظمنا المرجع الحقيقي، لما وجدت أي شيء على الإطلاق.
لماذا تُعد ملفات .pth وsitecustomize.py حلماً للبرمجيات الخبيثة
كانت ميزتان في تسلسل بدء تشغيل بايثون تجعلان هذا الهجوم شديد الخطورة، والحقيقة أن معظم مطوري بايثون لم يفكروا فيهما قط. الأولى: أي ملف .pth في site-packages يُعالَج عند بدء المفسِّر، وتُنفَّذ الأسطر التي تبدأ بـ import. احتوى ملف cook.pth ذو الـ12 بايت على سطر واحد فقط هو import cook، وكان ذلك كافياً لتشغيل برنامج السرقة في كل عملية بايثون على الجهاز. والثانية: يستورد بايثون تلقائياً وحدة باسم sitecustomize عند البدء إذا وجدها في المسار. وضع المهاجمون وحدتهم الخاصة، التي شغّلت عملية خلفية منفصلة تتصل كل ساعة بملف نصي مستضاف على GitHub، وتحمّل أي شيفرة يشير إليها المهاجم، وتنفذها عبر exec().
هذا أخطر بكثير من حزمة خبيثة تقليدية تحمل setup.py ملوثاً أو استيراداً سيئاً في __init__.py. فتلك تُنفَّذ عند تثبيت الحزمة أو استيرادها. أما هذه فكانت تُنفَّذ عند تشغيل أي شيء بلغة بايثون. تشغيل pytest في CI. أمر pip list الذي كتبته لتتحقق مما حدث. مُنسِّق black في محرّرك. كل استدعاء جديد كان يعيد تفعيل الفخ.
يستحق الأمر التوقف عند ما جمعه برنامج السرقة فعلاً، فالقائمة تشبه خريطة لحياة المطور الحديثة: بيانات اعتماد AWS وAzure وgcloud، وملفات kubeconfig، ورموز GitHub CLI، ومفاتيح SSH وGPG، ومخازن كلمات مرور المتصفح، وسجلات الأوامر في الصدفة، وأسرار CI من GitLab runners وJenkins، وبيانات اعتماد Terraform، ومتغيرات البيئة لعمليات أخرى مقروءة عبر /proc/<pid>/environ، وبيانات اعتماد بيانات تعريف نسخة AWS، ورموز Claude Code وCursor وCopilot وعشرات أدوات البرمجة بالذكاء الاصطناعي الأخرى، إضافة إلى مفاتيح مزودي LLM المخزنة في قاعدة بيانات الوكيل الخاصة بـ LiteLLM. أمر pip install واحد، ويصبح المهاجم يمتلك عملياً هويتك كاملة كمهندس.
لم تكن بحاجة لاستيراد litellm. كان تشغيل أي مفسِّر بايثون على الجهاز كافياً لتشغيل برنامج السرقة.
تثبيت الهاشات وملفات القفل: ضروري، لكنه غير كافٍ
كل قائمة تحقق لأمن سلسلة التوريد تبدأ بملفات القفل وتثبيت الهاشات، وهذا صحيح. إذا ثبّتّ الهاشات باستخدام pip install --require-hashes، أو استخدمت أداة مثل uv أو Poetry مع هاشات مقفلة، فإن أي ملف wheel يتغير بصمت بعد أن فحصته يُرفض. وهذه حماية حقيقية ضد فئة كاملة من الهجمات، مثل العبث بالسجل، وتسميم المرايا، واستبدال الملفات.
لكن لنكن صريحين بشأن ما لا تفعله هنا: لا شيء، على الأقل بالنسبة لمن قام بالترقية أولاً. يضمن تثبيت الهاش أن الملف الذي تثبّته غداً مطابق بايتاً ببايت لما ثبّتّه اليوم. وفي هذا الهجوم، كانت ملفات wheel الخبيثة هي الملفات الرسمية نفسها: صادرة من الجهة الأصلية، ومبنية عبر خط الإنتاج، وبهاشات متسقة. وفور أن نفّذ أحدهم ترقيته الروتينية، وحدّد الإصدار الجديد، وأودع ملف القفل المحدّث، ثُبّتت الهاشات الخبيثة داخل المستودع مع فحص CI أخضر. يحميك التثبيت من التغيير، لكنه لا يقول شيئاً عن النية.
حيث يتألق تثبيت الهاشات فعلاً في قصة كهذه هو نطاق الضرر. فالفرق التي تثبّت الهاشات ولها جدول ترقيات متعمد لا تركب تلقائياً كل إصدار في الساعة التي يُنشر فيها. فإذا كانت سياستك هي «ترقية التبعيات عبر طلب دمج أسبوعي، مع فترة تهدئة من بضعة أيام لأي شيء غير إصلاح حرج»، فإن سحب الحزمة من PyPI في 13:23 بتوقيت UTC وقع قبل أن تُفتح نافذة الترقية لديك أصلاً. وكثيراً ما تتفوق العملية المملة على الأداة الذكية، أكثر مما نودّ الاعتراف.
إذن احتفظ بملفات القفل، وولّدها مع الهاشات. لكن توقف عن إقناع نفسك بأنها دفاع ضد خط بناء مشرف مخترق؛ فهي دفاع ضد الانجراف والعبث، وحاجز بطيء أمام الإصدارات السيئة. هذا كل شيء.

ربط المصدر بالإثبات كان سيكشف هذا الهجوم
وهنا الوجه الآخر المزعج: هذا الهجوم بالتحديد كان يحمل شذوذاً واضحاً وقابلاً للكشف، والأدوات اللازمة لاكتشافه موجودة الآن. لم تُبنَ ملفات wheel الخبيثة من المستودع العام، بل من مستودع شبيه أنشأه المهاجم. وأي خطوة تحقق تسأل «هل بُني هذا الملف من الالتزام والمستودع المذكورين؟» كانت ستفشل بصوت عالٍ.
هذا بالضبط ما يوفره إثبات المصدر (provenance attestation). يدعم PyPI توثيقات PEP 740: عندما تُنشر الحزمة عبر Trusted Publishers من GitHub Actions، يستطيع سير العمل توليد بيان موقّع يربط الملف بالمستودع وسير العمل والالتزام الدقيق الذي أنتجه. وبعدها يصبح التحقق من جهة المستهلك آلياً: تحقّق من التوثيق، وتأكد أن المستودع المصدر هو BerriAI/litellm لا Berri-AI/litellm. تنتمي حِزم Sigstore وإثباتات SLSA والتحقق بأسلوب pip-audit إلى هذه العائلة.
الفجوة اليوم في التبنّي والتطبيق. فالتوثيقات اختيارية بالنسبة للناشرين، ونادراً ما يتحقق أحد منها وقت التثبيت حتى عندما تكون موجودة. وهذا الجزء هو ما يجب أن يتغير، وهو يشبه نقطة سبق أن تناولناها عن أن البنية التحتية للثقة لا تعمل إلا إذا تحقق منها أحد فعلاً: التوقيع الذي لا يتحقق منه أحد مجرد زخرفة. وإذا كانت مؤسستك تشغّل فهرس حزم أو وكيلاً خاصاً بها، وإذا كان لديك أكثر من حفنة من المهندسين، فيجب أن تفعل ذلك، فسياسة «ارفض الملفات التي لا يمكن التحقق من مصدرها المتوقع» قابلة للتطبيق مركزياً، مرة واحدة، لكل مطور.
وهناك درس أدق أيضاً. احتاج المهاجم إلى ثلاث بيانات اعتماد لمطورين: واحدة لتحويل سير العمل، وواحدة للموافقة، وواحدة لإخفاء الأثر. وهذا فشل منهجي في نظافة بيانات الاعتماد، لا خطأ واحد. كانت رموز Trusted Publisher القصيرة العمر المعتمدة على OIDC في PyPI ستقلّص سطح الهجوم بشكل كبير، لأنه لا يوجد رمز طويل العمر جالس في ملف سجل لتسرقه. وإذا كان مشروعك ما زال ينشر باستخدام رموز API مخزنة كأسرار في CI، فهذه هي اللحظة المناسبة للتغيير.
التثبيت في بيئة معزولة وحجة فترات التهدئة
افترض أسوأ السيناريوهات: إصدار خبيث يتجاوز نظافة ملف القفل لديك وفحوصات المصدر، لأن ذلك سيحدث يوماً ما. فما الذي يبقى؟ الاحتواء. لم يعد السؤال «كيف نمنع الحزم السيئة؟»، بل صار «عندما تعمل حزمة سيئة، إلى ماذا تستطيع الوصول؟»
بالنسبة لـ CI، الجواب قابل للتحقيق فعلاً. يجب أن تعمل عمليات البناء في بيئات مؤقتة (ephemeral) بلا بيانات اعتماد طويلة العمر، مع رموز محدودة النطاق وتصفية للحركة الخارجة. سلوك برنامج السرقة في LiteLLM صاخب على الشبكة إن كنت تراقبها: طلب POST إلى نطاق غير مألوف مع ترويسة مصادقة مخصصة، وجلب من عنوان GitHub خام في حلقة كل ساعة، والأهم من ذلك كله، عملية بايثون تستعلم عن 169.254.169.254، وهو خدمة بيانات تعريف نسخة AWS. نادراً ما تحتاج شيفرة التطبيق لأي سبب مشروع أن تتصل بـ IMDS مباشرة، فحزمة SDK تتولى ذلك. أطلق تنبيهاً عند قيام عمليات بايثون بالوصول إلى هذه النقطة، وستلتقط عائلة كاملة من لصوص بيانات الاعتماد، لا هذا البرنامج وحده. كنا قد كتبنا سابقاً عن النقاط العمياء في تسجيل المصادقة السحابية، وهذه واحدة منها، وإغلاقها رخيص.
أجهزة المطورين أصعب، ولن أتظاهر بغير ذلك. الحاسوب المحمول الذي يشغّل فيه المستخدم نفسه محرّره ومتصفحه ووكيل SSH وأوامر pip install عشوائية، هو مائدة مفتوحة للص. الخطوات العملية: نفّذ عمليات التثبيت الاستكشافية داخل الحاويات أو devcontainers بدلاً من بيئتك المضيفة؛ وأبقِ تثبيت الحزم داخل devcontainer الخاص بالمشروع حتى لا يصل ملف sitecustomize.py الخبيث أبداً إلى site-packages الجهاز؛ وتعامل مع ~/.aws/credentials كعبء. فاستخدام SSO مع رموز قصيرة العمر (aws sso login وgcloud auth) يعني وجود قدر أقل بكثير من البيانات المخزنة على القرص لتُسرق. لا شيء من هذا غريب أو معقد، إنه مجرد احتكاك، والاحتكاك هو الغاية.
- ثبّت الإصدارات والهاشات في ملف قفل، لكن اعامل الترقيات كأحداث مقصودة لا كضجيج في الخلفية.
- فرض فترة تهدئة من 48 إلى 72 ساعة قبل وصول ترقيات التبعيات غير الحرجة إلى بنيات الإنتاج. معظم الإصدارات الخبيثة يُسحب خلال ساعات.
- تحقق من توثيقات المصدر حيثما وجدت، وفضّل الحزم المنشورة عبر Trusted Publishers باستخدام OIDC على رموز API طويلة العمر.
- شغّل عمليات بناء CI في بيئات مؤقتة خالية من بيانات الاعتماد مع تصفية للحركة الخارجة، وأطلق تنبيهاً عند أي عملية بايثون تصل إلى نقاط بيانات تعريف النسخ.
- ثبّت التطبيقات وشغّل الشيفرة الخارجية في حاويات على أجهزة المطورين، واستبدل ملفات بيانات الاعتماد السحابية الثابتة بجلسات SSO قصيرة العمر.
- افحص بيئتك بحثاً عن مؤشرات الاختراق إن سحبت أي شيء خلال النافذة:
cook.pthأوcook.pyأو ملفsitecustomize.pyغريب في site-packages، وكذلك/tmp/.pc_once_*أو/tmp/remote_code_loader.lockعلى القرص.
ما الذي يكشفه هذا الهجوم عن موضع الخطر الحقيقي
لاحظ ما لم يكلّف المهاجم نفسه عناء فعله. لم يستغل ثغرة في الشيفرة، ولم يحتج إلى ثغرة يوم صفر. ذهب إلى أسهل هدف في البرمجيات الحديثة: البشر والآلات التي تحمل بيانات اعتماد دائمة. ثلاثة رموز GitHub CLI، يبدو أنها استُخرجت من سجلات نشر داخلي مكشوف على الإنترنت، كانت كافية للاستيلاء على خط إصدار يخدم ملايين التنزيلات. وهذا النمط نفسه نراه يتكرر في الصناعة: أسرار تتسرب إلى السجلات والوكلاء وملفات الإعداد، وكل واحد منها مفتاح عام يفتح كل الأبواب. وإن كنت تشغّل نسخة داخلية من أي شيء وتعرضها للإنترنت، فسجلاتها صارت جزءاً من نموذج التهديد لديك. وينبغي أن تُقلق هذه الجملة بعض الناس.
هناك أيضاً مشكلة السرعة تستحق التسمية. LiteLLM مشروع سريع الحركة، عشرات الآلاف من الالتزامات وإصدارات متكررة، ويقع في قلب مكدّس أدوات الذكاء الاصطناعي، إذ تسحبه أطر العمل وحِزم SDK التي لا يعرف كثير من الفرق حتى أنها تعتمد عليه. فالوصول غير المباشر يعني أنك لا تختار مدى تعرضك باختيار تبعياتك المباشرة. لم يعد SBOM في هذا العالم ورقاً للامتثال، بل هو الطريقة التي تجيب بها عن سؤال «هل نحن متأثرون؟» خلال دقائق بدلاً من أيام. والأدوات التي تولّد قائمة المواد من أدلة البناء الفعلية لا من تحليل ملفات البيان ستلتقط التبعيات التي لا يذكرها requirements.txt لديك.

رأيي: كانت استجابة الصناعة لهجمات سلسلة التوريد مبالغة في مسرح الوقاية، أي ماسحات تكتشف الإصدارات المعروفة بأنها سيئة بعد وقوع الضرر، ومقصّرة في الضوابط الجافة غير البرّاقة التي كانت ستغيّر هذه القصة. رموز نشر قصيرة العمر. التحقق من المصدر عند الوكيل. فترات التهدئة. تنبيهات الحركة الخارجة نحو نقاط البيانات الوصفية. التثبيت في بيئات معزولة. لا تحتاج أي من هذه إلى بحث علمي، بل هي قرارات إعداد متاحة اليوم، ومعظم الفرق لم تطبق أياً منها. احتاج مهاجمو LiteLLM إلى سطر سيئ واحد في إعداد البناء وثلاثة رموز مسروقة. كانت الدفاعات أعلاه ستجبرهم على اختراق أربع طبقات مستقلة بدلاً من واحدة. هذا هو معنى الدفاع متعدد الطبقات: ليس مزيداً من الماسحات، بل مزيداً من الطرق المستقلة التي يمكن أن تفشل بها الحماية. وهي الفلسفة نفسها وراء هندسة الأنظمة التي تقاوم الكوارث بدلاً من تلك التي تكتفي بالرجاء.
الإصدار الخبيث التالي يُخطَّط له بالفعل من أحدهم. لن تمنع نشره، لكن يمكنك تماماً أن تقرر ما إذا كان سيعمل ومفاتيح AWS الخاصة بك في متناوله، ووكيل SSH مفتوح، ومسار شبكي واضح إلى نطاق يسيطر عليه المهاجم، أو ما إذا كان سيموت داخل حاوية مؤقتة بلا بيانات اعتماد وخلف جدار ناري. هذا القرار يُتخذ الآن، في إعداد CI وإعداد devcontainer الخاصين بك، لا في اليوم الذي يُنشر فيه تقرير الحادث.


