PyPI सप्लाई चेन अटैक: LiteLLM से सबक
LiteLLM के PyPI समझौते में हर wheel में क्रेडेंशियल चुराने वाला malware था। जानिए hash pinning और lockfiles क्या रोक सकते थे, और क्या नहीं।

तीन फ़ाइलें। एक अब तक देखे गए सबसे खतरनाक Python supply chain attacks में से एक का पूरा निशान बस इतना है: 12-byte का cook.pth, 22 KB की cook.py, और 25 KB की sitecustomize.py। जब LiteLLM 1.107.0 PyPI पर आया, तो ये तीनों फ़ाइलें उसके सातों wheels के अंदर चली गईं, और Python की startup व्यवस्था की वजह से ये हर संक्रमित होस्ट पर हर Python process में चलीं: आपके ऐप में, आपके CI runner में, pip में, यहाँ तक कि आपके editor के language server में भी। आपको litellm import करने की ज़रूरत नहीं थी। उसे चलाने की भी नहीं। कोई भी Python interpreter शुरू करना काफ़ी था।
LiteLLM टीम ने समझौते की पुष्टि की और PyPI ने कुछ घंटों के भीतर release वापस ले लिया, लेकिन यह खिड़की इतनी लंबी थी कि नुकसान हो जाए। दुर्भावनापूर्ण Docker images, जिनमें latest tag भी शामिल था, लगभग ढाई घंटे तक उपलब्ध रहीं। अगर इस दौरान आपका CI उन्हें pull कर रहा था, तो stealer SSH keys, cloud credentials, browser passwords, CI/CD secrets और AI tool tokens पहले ही इकट्ठा कर चुका होगा, उन्हें encrypt करके किसी attacker के नियंत्रण वाले domain पर भेज चुका होगा। यह कोई typosquat या मिलता-जुलता नकली पैकेज नहीं था। यह असली पैकेज था, असली pipeline से बना, असली release process से signed। और इससे आपको dependency पर भरोसे के बारे में अपनी सोच बदलनी चाहिए।
तीन फ़ाइलों ने आधिकारिक release pipeline को कैसे हाईजैक किया
हमलावर ने PyPI को हैक नहीं किया, और न ही सार्वजनिक repository में चुपचाप कोई commit डाला। upstream GitHub source साफ़ है। LiteLLM की incident report के अनुसार, हमलावर के पास तीन developers के GitHub CLI credentials थे, जो कथित तौर पर इंटरनेट पर खुले एक internal LiteLLM deployment के logs से निकाले गए थे। एक organization owner के token से उन्होंने branch protection को बायपास किया और निजी release workflow को असली BerriAI/litellm की जगह एक मिलते-जुलते repository, Berri-AI/litellm, की ओर मोड़ दिया। एक दूसरे developer के credential ने approval gate पास करा दिया। फिर आधिकारिक pipeline ने हमलावर के artifacts build, sign और publish कर दिए।
असली injection काफ़ी छोटा और चतुराई भरा था। sdist की pyproject.toml में बस एक अतिरिक्त लाइन:
[tool.maturin]
python-source = "."
data = "cook" # <-- injected, copies cook/ into the wheel
python-packages = ["litellm_cli"] # legitimate, present upstream
वह data = "cook" directive maturin को बताता है कि wheel के purelib path में एक cook/ directory bundle करे, और pip उसे आज्ञाकारी ढंग से site-packages में install कर देता है। sdist में वह folder ही नहीं था, इसलिए कोई इंसान release पर सरसरी नज़र डाले तो उसे बस एक रहस्यमयी लाइन दिखेगी और कुछ नहीं। payload source में नहीं, build artifacts में छिपा था। git history की समीक्षा करते, जिसे हम में से ज़्यादातर लोग ground truth मानते हैं, आपको कुछ भी नज़र नहीं आता।
.pth फ़ाइलें और sitecustomize.py malware के लिए सपना क्यों हैं
Python के startup क्रम की दो विशेषताओं ने इस हमले को असाधारण रूप से शक्तिशाली बनाया, और ईमानदारी से कहूँ तो ये ऐसी विशेषताएँ हैं जिनके बारे में ज़्यादातर Python developers ने कभी सोचा ही नहीं। पहली: site-packages में मौजूद कोई भी .pth फ़ाइल interpreter startup पर process होती है, और import से शुरू होने वाली लाइनें execute होती हैं। 12-byte की cook.pth में ठीक एक लाइन थी, import cook, और इतना ही मशीन की हर Python process में stealer चलाने के लिए काफ़ी था। दूसरी: अगर path पर sitecustomize नाम का module मिले, तो Python startup पर उसे अपने-आप import कर लेता है। हमलावरों ने अपना वाला डाला, जो एक detached daemon fork करता था, जो हर घंटे GitHub-hosted text file पर “घर फ़ोन” करता, हमलावर जो भी code बताए उसे डाउनलोड करता, और उसे exec() से चलाता।
यह किसी आम malicious package से कहीं बड़ी बात है, जिसमें setup.py में ज़हर हो या __init__.py में गलत import हो। वे तब execute होते हैं जब आप package install करते हैं या import करते हैं। यह तब execute हुआ जब आपने Python में कुछ भी चलाया। आपकी CI में pytest run। वह pip list जो आपने यह देखने के लिए टाइप किया कि क्या हुआ। आपके IDE का black formatter। हर बार की invocation ने जाल फिर से तैयार कर दिया।
यह देखना ज़रूरी है कि stealer ने असल में क्या इकट्ठा किया, क्योंकि सूची आधुनिक developer जीवन का नक्शा जैसी लगती है: AWS, Azure और gcloud credentials, kubeconfigs, GitHub CLI tokens, SSH और GPG keys, browser password stores, shell history, GitLab runners और Jenkins से CI secrets, Terraform credentials, /proc/<pid>/environ से पढ़े गए दूसरी processes के environment variables, AWS instance metadata credentials, और, एक भयानक विडंबना में, Claude Code, Cursor, Copilot और दर्जनों दूसरे AI coding tools के tokens, साथ ही LiteLLM के अपने proxy database में रखी LLM provider keys। बस एक pip install, और हमलावर के पास लगभग आपकी पूरी engineer के रूप में पहचान होती है।
आपको litellm import करने की ज़रूरत नहीं थी। होस्ट पर कोई भी Python interpreter शुरू करना stealer चलाने के लिए काफ़ी था।
Hash Pinning और Lockfiles: ज़रूरी हैं, पर काफ़ी नहीं
हर supply chain security checklist lockfiles और hash pinning से शुरू होती है, और होनी भी चाहिए। अगर आप pip install --require-hashes से hashes pin करते हैं, या uv या Poetry जैसे tool का locked hashes के साथ इस्तेमाल करते हैं, तो जो wheel आपके जाँचने के बाद चुपचाप बदल जाए, वह reject हो जाएगी। registry tampering, mirror poisoning और artifact replacement जैसे हमलों की एक पूरी श्रेणी के खिलाफ यह असली सुरक्षा है।
लेकिन ईमानदारी से देखें कि यहाँ यह क्या करता है: कम से कम उनके लिए तो कुछ नहीं, जिन्होंने सबसे पहले upgrade किया। Hash pinning गारंटी देता है कि कल आप जो artifact install करेंगे वह आज वाले के साथ byte-for-byte एक जैसा होगा। इस हमले में दुर्भावनापूर्ण wheels ही आधिकारिक artifacts थीं: पहले-पक्ष की, pipeline से बनी, लगातार hash की गई। जैसे ही किसी ने रोज़ का upgrade चलाया, नया version resolve किया और अपडेटेड lockfile commit किया, दुर्भावनापूर्ण hashes हरे CI check के साथ repo में pin हो गए। Pinning आपको बदलाव से बचाता है; इरादे के बारे में वह कुछ नहीं कहता।
ऐसी कहानी में pinning सचमुच कहाँ काम आता है, वह है blast radius। जिन टीमों के पास pinned hashes और सोच-समझकर upgrade cadence है, वे हर release के आते ही अपने-आप उसे नहीं अपना लेतीं। अगर आपकी नीति है कि “dependencies का upgrade हफ़्ते के PR में होगा, और गैर-critical fix के लिए कुछ दिनों का cooling-off period रहेगा”, तो PyPI से 13:23 UTC पर हटाए जाने से पहले ही आपकी upgrade window खुली ही नहीं थी। उबाऊ process अक्सर चतुर tooling से बेहतर साबित होता है, भले ही हम इसे मानना पसंद न करें।
तो lockfiles रखें। उन्हें hashes के साथ बनाएँ। लेकिन खुद को यह मानना बंद करें कि वे compromised maintainer pipeline के खिलाफ सुरक्षा हैं। वे drift और tampering के खिलाफ सुरक्षा हैं, और खराब releases के खिलाफ एक छोटा-सा speed bump। बस इतना ही।

Provenance Attestation इस हमले को पकड़ लेता
यहाँ असहज करने वाला दूसरा पहलू है: इस खास हमले में एक साफ़ और पकड़ में आने वाली गड़बड़ी थी, और उसे पकड़ने वाला tooling इकोसिस्टम में अभी मौजूद है। दुर्भावनापूर्ण wheels सार्वजनिक repository से build नहीं हुए थे। वे हमलावर द्वारा बनाए गए मिलते-जुलते repo से build हुए थे। कोई भी verification step जो पूछे कि “क्या यह artifact उसी commit और repository से बना है जिसका वह दावा करता है?” ज़ोर से फेल हो जाता।
Provenance attestation यही सुविधा देता है। PyPI PEP 740 attestations सपोर्ट करता है: जब कोई package Trusted Publishers के ज़रिए GitHub Actions से publish होता है, तो workflow एक signed statement बना सकता है जो artifact को उसी सटीक repository, workflow और commit से जोड़ता है जिसने उसे बनाया। उपभोक्ता की ओर से जाँच फिर यांत्रिक हो जाती है: attestation verify करें, पुष्टि करें कि source repo BerriAI/litellm है, Berri-AI/litellm नहीं। Sigstore bundles, SLSA provenance और pip-audit जैसी verification इसी परिवार की चीज़ें हैं।
आज की कमी adoption और enforcement की है। Attestations publishers के लिए opt-in हैं, और मौजूद होने पर भी install के समय उन्हें लगभग कोई verify नहीं करता। यही वह हिस्सा है जिसे बदलना होगा, और यह उस बात से मेल खाता है जो हम पहले trust infrastructure तभी काम करता है जब कोई उसे सचमुच जाँचे पर कह चुके हैं: जिस signature को कोई verify न करे, वह सिर्फ़ सजावट है। अगर आपकी organization अपना package index या proxy चलाती है, और अगर आपके पास मुट्ठी भर से ज़्यादा engineers हैं तो चलाना चाहिए, तो “expected source से verifiable provenance न रखने वाले artifacts को reject करें” एक ऐसी नीति है जिसे आप एक बार, केंद्रीय रूप से, हर developer के लिए लागू कर सकते हैं।
एक बारीक सबक भी है। हमलावर को तीन developer credentials चाहिए थे: एक workflow को मोड़ने के लिए, एक approve करने के लिए, एक बचाने के लिए। यह किसी एक गलती के बजाय credential hygiene की व्यवस्थित विफलता है। PyPI पर short-lived, OIDC-आधारित Trusted Publisher tokens हमले की सतह को नाटकीय रूप से घटा देते, क्योंकि कोई long-lived token किसी log फ़ाइल में पड़ा नहीं रहता जिसे चुराया जा सके। अगर आपका प्रोजेक्ट अब भी CI secrets में रखे API tokens से publish करता है, तो यह आपके लिए चेतावनी है।
Sandboxed installs और cooling-off periods का मामला
सबसे बुरे हालात मान लें: कोई दुर्भावनापूर्ण release आपकी lockfile hygiene और provenance checks दोनों को चकमा दे देती है, क्योंकि एक दिन ऐसा होगा ही। फिर बचता क्या है? Containment। सवाल “हम बुरे packages को कैसे रोकें” से बदलकर “जब कोई बुरा package चलता है, तो वह किस तक पहुँच सकता है?” हो जाता है।
CI के लिए जवाब वास्तव में हासिल किया जा सकता है। Builds ऐसे ephemeral environments में चलने चाहिए जिनमें कोई long-lived credentials न हों, scoped tokens हों, और egress filtering हो। अगर आप नेटवर्क पर नज़र रखें, तो LiteLLM stealer का व्यवहार काफ़ी ज़ोरदार है: किसी अनजान domain पर custom auth header के साथ POST, हर घंटे के loop में raw GitHub URL से fetch, और सबसे बड़ा संकेत, एक Python process का 169.254.169.254 को क्वेरी करना, जो AWS instance metadata service है। Application code के पास IMDS को सीधे छूने का शायद ही कोई जायज़ कारण होता है; SDK यह काम खुद कर लेता है। Python processes का उस endpoint को छूने पर alert लगाएँ, और आप सिर्फ़ इसी नहीं, credential चोरों के पूरे परिवार को पकड़ लेंगे। हमने पहले cloud authentication logging की blind spots पर लिखा है। यह उनमें से एक है, और इसे बंद करना सस्ता है।
Developer machines कठिन हैं, और मैं इसका दिखावा नहीं करूँगा। वह laptop जहाँ वही user अपना editor, browser, SSH agent और मनमर्ज़ी के pip install चलाता है, stealer के लिए बुफ़े है। व्यावहारिक कदम ये हैं: exploratory installs को host environment के बजाय containers या devcontainers में करें; project devcontainer के अंदर ही package installs रखें ताकि दुर्भावनापूर्ण sitecustomize.py कभी आपके host के site-packages में न पहुँचे; और ~/.aws/credentials को देनदारी मानें। short-lived tokens वाले SSO (aws sso login, gcloud auth) का मतलब है कि डिस्क पर चुराने लायक बहुत कम पड़ा रहता है। इनमें से कुछ भी विदेशी नहीं है। यह बस घर्षण है, और घर्षण ही मकसद है।
- Lockfile में versions और hashes दोनों pin करें, पर upgrades को पृष्ठभूमि का शोर नहीं, बल्कि सोच-समझकर लिया गया फ़ैसला मानें।
- गैर-critical dependency upgrades को production builds तक पहुँचने से पहले 48 से 72 घंटे का cooling-off window लगाएँ। ज़्यादातर दुर्भावनापूर्ण releases कुछ ही घंटों में वापस ले ली जाती हैं।
- जहाँ provenance attestations मौजूद हों, उन्हें verify करें, और long-lived API tokens के बजाय OIDC के साथ Trusted Publishers से publish हुए packages को प्राथमिकता दें।
- CI builds को ephemeral, credential-free environments में egress filtering के साथ चलाएँ; किसी भी Python process के instance metadata endpoints छूने पर alert लगाएँ।
- Developer machines पर third-party code को containers में install और चलाएँ, और static cloud credential files की जगह short-lived SSO sessions रखें।
- अगर इस खिड़की में आपने कुछ भी pull किया है तो IoCs के लिए अपने environment का ऑडिट करें: site-packages में
cook.pth,cook.py, या कोई आवाराsitecustomize.py, और डिस्क पर/tmp/.pc_once_*या/tmp/remote_code_loader.lock।
यह हमला बताता है कि असली जोखिम कहाँ रहता है
देखिए हमलावर ने किस चीज़ की परवाह ही नहीं की। उन्होंने कोई code vulnerability इस्तेमाल नहीं की। उन्हें zero-day की ज़रूरत नहीं पड़ी। वे आधुनिक software के सबसे नरम निशाने की ओर गए: वे इंसान और मशीनें जो स्थायी credentials थामे रहती हैं। तीन GitHub CLI tokens, जो कथित तौर पर इंटरनेट पर खुले एक internal deployment के logs से निकाले गए थे, लाखों डाउनलोड सेवा देने वाली release pipeline पर कब्ज़ा करने के लिए काफ़ी थे। यह वही पैटर्न है जो इंडस्ट्री में बार-बार दिखता है: secrets logs, agents और config files में फैल जाते हैं, और हर एक एक skeleton key बन जाता है। अगर आप कोई भी चीज़ internal instance के रूप में चलाते हैं और उसे इंटरनेट पर खोलते हैं, तो उसके logs अब आपके threat model का हिस्सा हैं। यह वाक्य कुछ लोगों को असहज कर देना चाहिए।
एक रफ़्तार वाली समस्या का नाम भी लेना ज़रूरी है। LiteLLM एक तेज़ी से बढ़ता project है, दसियों हज़ार commits, बार-बार releases के साथ, और यह AI tooling stack के केंद्र में बैठा है, उन frameworks और SDKs द्वारा खींचा गया जिनके बारे में कई टीमों को पता ही नहीं कि वे इस पर निर्भर हैं। Transitive पहुँच का मतलब है कि आप अपने direct dependencies चुनकर अपना exposure नहीं चुन सकते। इस दुनिया में SBOM सिर्फ़ compliance की कागज़ी कार्रवाई नहीं रहता; यही वह तरीका है जिससे आप “क्या हम प्रभावित हैं?” का जवाब दिनों के बजाय मिनटों में दे पाते हैं। जो tools manifest scraping के बजाय असली build evidence से bill of materials बनाते हैं, वे उन dependencies को भी पकड़ लेंगे जिनका ज़िक्र आपकी requirements.txt में नहीं है।

मेरी राय: supply chain हमलों पर इंडस्ट्री की प्रतिक्रिया prevention theater पर ज़रूरत से ज़्यादा झुकी रही है, यानी ऐसे scanners जो घटना के बाद ज्ञात-बुरे versions को flag करते हैं, और उन नीरस controls पर कम ज़ोर दिया गया जो इस कहानी को बदल सकते थे। Short-lived publishing credentials। Proxy पर provenance verification। Cooling-off periods। Metadata endpoints पर egress alerts। Sandboxed installs। इनमें से कोई भी research problem नहीं है। ये configuration के फ़ैसले हैं, जो आज उपलब्ध हैं, और ज़्यादातर टीमों ने इनमें से एक भी लागू नहीं किया। LiteLLM के हमलावरों को build config में बस एक गलत लाइन और तीन चुराए गए tokens चाहिए थे। ऊपर के बचाव उन्हें एक के बजाय चार स्वतंत्र परतों को तोड़ने पर मजबूर करते। defense in depth का असली अर्थ यही है: ज़्यादा scanners नहीं, बल्कि ज़्यादा स्वतंत्र तरीके जिनसे हमला नाकाम हो जाए। यह उसी सोच का हिस्सा है जो ऐसे systems बनाने के पीछे है जो तबाही का प्रतिरोध करें, न कि सिर्फ़ उम्मीद करें कि सब ठीक रहेगा।
अगली समझौता की गई release की योजना कोई न कोई बना ही रहा है। आप उसे प्रकाशित होने से नहीं रोक पाएँगे। लेकिन आप तय कर सकते हैं कि वह आपकी AWS keys पहुँच में, आपका SSH agent खुला और हमलावर के domain तक साफ़ नेटवर्क रास्ता लेकर चले, या वह किसी ephemeral container में, बिना credentials और firewall के साथ, खत्म हो जाए। वह फ़ैसला अभी लिया जाता है, आपकी CI config और devcontainer setup में, न कि उस दिन जब incident report आएगी।


