SSD Nodes Learn 🎉 VPS $5.50/माह से
गाइड Matt Connorलेखक: Matt Connor · अपडेट किया गया: 2026-08-15

npm supply-chain attack से सर्वर को सुरक्षित कैसे रखें

npm supply-chain attack आपके Node app को कैसे प्रभावित करते हैं। malicious patch, postinstall scripts और typosquats से बचने के लिए सही deploy practice के बारे में विस्तार से जानें।

npm supply-chain attack क्या है और यह आपके सर्वर को कैसे प्रभावित करता है

npm supply-chain attack आपके सर्वर तक उस पैकेज के माध्यम से पहुँचता है जिसे आपने इंस्टॉल करने के लिए चुना है। इसमें किसी open port का उपयोग नहीं होता और न ही इसमें exploit का कोई चरण शामिल है। npm (node package manager) कोड इंस्टॉल करता है, और कोड इंस्टॉल करने का अर्थ है कोड को रन करना। इसलिए, एक छोटा Node application उन सैकड़ों पैकेजों को खींच लेता है जिन्हें आपने कभी पढ़ा नहीं है, और उनमें से कोई भी पैकेज एक घंटे बाद अपना नया version जारी कर सकता है।

आपका deploy एक malicious version को fetch कर लेता है क्योंकि आपके install command ने सबसे नए matching version की मांग की थी। वह कोड फिर उसी व्यक्ति के privileges के साथ रन होता है जिसने install command चलाया था। नीचे दी गई हर बात इन्हीं दो वाक्यों का परिणाम है।

ये स्थितियाँ इस आधार पर क्रमबद्ध हैं कि वे एक व्यक्ति द्वारा एक VPS पर एक Node app को deploy करने के दौरान कितनी बार घटित होती हैं। यह क्रम वह नहीं है जिसका उपयोग कोई बड़ी कंपनी करेगी, क्योंकि एक बड़ी कंपनी के पास internal registry, एक review team और public registry का एक mirror होता है। आपके पास केवल एक deploy script है।

स्थिति 1: एक मेंटेनर अकाउंट हैक हो जाता है और एक पैच पब्लिश करता है

npm registry किसी मौजूदा version की सामग्री को बदलने की अनुमति नहीं देती है। इसलिए, जो हमलावर किसी मेंटेनर को phish करता है या publish token चुराता है, वह 4.18.2 को rewrite नहीं कर सकता। वे 4.18.3 पब्लिश करते हैं।

अपनी package.json को देखें। "express": "^4.18.2" जैसी लाइन का मतलब version 4.18.2 नहीं है। Caret का अर्थ है "इस version या उससे ऊपर का कोई भी 4.x version", और ~4.18.2 का अर्थ है "कोई भी 4.18.x"। npm install रन होने के समय उस range को resolve करता है, इसलिए एक ही git commit, जिसे एक ही दोपहर में दो बार deploy किया गया हो, कोड के दो अलग-अलग सेट install कर सकता है। यह अंतर ही attack surface है। इसे खोलने के लिए आपकी मशीन पर किसी चीज़ का compromised होना जरूरी नहीं है।

दुर्भावनापूर्ण releases (malicious releases) की आमतौर पर रिपोर्ट की जाती है और उन्हें हटा दिया जाता है, लेकिन यह हटाना लोगों द्वारा उन्हें install करने के बाद होता है। जिस किसी ने उस समय के दौरान deploy किया है, उसके disk पर वह कोड मौजूद है। जो pipeline हर बार चलने पर ranges को resolve करती है, वह बिना किसी के निर्णय लिए, सप्ताह में कई बार स्वचालित रूप से उस जोखिम के दायरे में आ जाती है।

आकार 2: एक इंस्टॉल स्क्रिप्ट उस उपयोगकर्ता के रूप में चलती है जो डिप्लॉयमेंट कर रहा है

किसी पैकेज का package.json अपने scripts ब्लॉक में preinstall, install, postinstall और prepare घोषित कर सकता है। npm इन्हें इंस्टॉलेशन के दौरान चलाता है। ये सैंडबॉक्स में नहीं होते और कोई भी इनकी समीक्षा नहीं करता है। ये शेल कमांड्स होते हैं जो उस उपयोगकर्ता के रूप में चलते हैं जिसने इंस्टॉल कमांड टाइप किया है, उसी उपयोगकर्ता की होम डायरेक्टरी में, उसी उपयोगकर्ता के नेटवर्क एक्सेस के साथ और उस शेल के पूर्ण वातावरण (environment) के साथ।

इसलिए उपयोगी प्रश्न यह नहीं है कि पैकेज क्या कर सकता है। बल्कि यह है कि वह उपयोगकर्ता क्या पढ़ सकता है। एक सामान्य डिप्लॉयमेंट बॉक्स पर इसका उत्तर है: ~/.npmrc जिसमें रजिस्ट्री टोकन होता है, ~/.ssh/id_ed25519 जिसे SSH (secure shell) के लिए डिप्लॉय की के रूप में उपयोग किया जाता है, ~/.aws/credentials, ~/.docker/config.json, और शेल में मौजूद हर एक्सपोर्टेड वेरिएबल, जहाँ आमतौर पर DATABASE_URL रहता है।

इस तरह के पेलोड को न तो निरंतरता (persistence) की आवश्यकता होती है और न ही प्रिविलेज एस्केलेशन की। यह कुछ फाइलें पढ़ता है, उन्हें HTTPS के माध्यम से एक होस्ट पर भेजता है, और 0 स्टेटस के साथ बाहर निकल जाता है। आपको कुछ भी दिखाई नहीं देता, क्योंकि npm डिफ़ॉल्ट रूप से इंस्टॉल-स्क्रिप्ट के आउटपुट को छिपा देता है। इसे बंद करें और देखें कि वास्तव में क्या चल रहा है:

npm ci --foreground-scripts

foreground-scripts, npm प्रक्रिया के साथ स्टैंडर्ड इनपुट, आउटपुट और एरर को साझा करता है, इसलिए बिल्ड स्क्रिप्ट्स आपके टर्मिनल में प्रिंट होती हैं, न कि उस बफर में जिसे npm इंस्टॉलेशन सफल होने पर हटा देता है।

आकृति 3: typosquats, और वह नाम जो आपने ठीक से टाइप नहीं किया

Typosquat एक ऐसा पैकेज है जिसे किसी लोकप्रिय पैकेज के नाम से मिलते-जुलते नाम के तहत प्रकाशित किया जाता है, जो गलत टाइप किए गए या गलत तरीके से पेस्ट किए गए install कमांड की प्रतीक्षा करता है। इसका तंत्र कमांड है, कोड नहीं, इसलिए lockfile यहाँ आपकी मदद नहीं करती है: आप एक बार गलत नाम जोड़ते हैं, और उसके बाद lockfile निष्ठापूर्वक उसे पिन कर देती है।

जो वेरिएंट व्यक्तियों के बजाय टीमों को फंसाता है, वह dependency confusion है। आपका आंतरिक पैकेज billing-utils कहलाता है और एक private registry पर रहता है। यदि public registry पर billing-utils नाम का कुछ भी मौजूद नहीं है, तो कोई भी इसे प्रकाशित कर सकता है। npm unscoped नामों को default public registry के विरुद्ध resolve करता है, इसलिए public कॉपी जीत सकती है। इसका समाधान एक scope है जिसका आप स्वामित्व रखते हैं, साथ ही उस scope के लिए एक registry मैपिंग, जो .npmrc में है:

@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}

अब @yourorg/billing-utils को केवल उस host से ही fetch किया जाता है, क्योंकि default registry से पहले scope-to-registry मैपिंग को देखा जाता है। एक unscoped आंतरिक नाम की कोई मैपिंग नहीं होती है, इसलिए इसकी कोई सुरक्षा नहीं होती है।

किसी भी नई dependency को जोड़ने से पहले, उसके डाउनलोड बैज के बजाय उसे देखें:

npm view some-lib repository.url maintainers time.created time.modified

पिछले महीने बनाया गया एक पैकेज, जिसे ऐसे account द्वारा प्रकाशित किया गया है जिसे आप किसी public repository से नहीं जोड़ सकते, छह साल के इतिहास वाले पैकेज से एक अलग जोखिम है। कोई भी तथ्य प्रमाण नहीं है। दोनों की जाँच करना आसान है।

आकार 4: वह dependency जिसका स्वामी चुपचाप बदल गया

Maintainers पैकेज सौंप देते हैं। यदि कोई थक जाता है और कोई अनजान व्यक्ति मदद की पेशकश करता है, तो publish करने के अधिकार स्थानांतरित हो जाते हैं, और इस पर निर्भर प्रोजेक्ट्स को इसकी कोई सूचना नहीं मिलती। कुछ भी breach नहीं होता है। आपने 2021 में जिस पर भरोसा किया था, वह अब किसी और के पास है।

यह सबसे धीमा आकार है और इसे पहचानना सबसे कठिन है, और कोई भी command सीधे इसका उत्तर नहीं देती। दो चीजें इसे सीमित करती हैं। किसी पैकेज को अपनाने से पहले यह जाँचें कि कौन publish कर सकता है, इसके लिए ऊपर दी गई npm view लाइन का उपयोग करें। फिर जब कोई पैकेज जिस पर आप वास्तव में निर्भर हैं, स्थानांतरित हो जाए, तो diff को पढ़ें:

npm diff --diff-name-only --diff=some-lib@1.4.2 --diff=some-lib@1.4.3
npm diff --diff=some-lib@1.4.2 --diff=some-lib@1.4.3

पहला प्रारूप केवल बदली हुई फाइलों के नाम प्रिंट करता है, जो आपके द्वारा उपयोग किए जाने वाले किसी भी पैकेज के हर upgrade पर करने के लिए पर्याप्त तेज है। एक patch release जो build script को छूती है, पैकेज root पर कोई फाइल जोड़ती है, या scripts ब्लॉक को edit करती है, उसे आपके सर्वर तक पहुँचने से पहले पूरा पढ़ना उचित है।

npm ci के साथ कमिट की गई lockfile से बिल्ड करें

package-lock.json ट्री में मौजूद हर पैकेज का सटीक वर्ज़न, वह URL जहाँ से वह आया है, हर tarball का sha512 इंटीग्रिटी हैश, और उसे किस पैकेज ने माँगा है, यह सब रिकॉर्ड करता है। इसे कमिट करें। यही एकमात्र फाइल है जो यह बताती है कि आपने वास्तव में किसका परीक्षण किया है।

फिर किसी भी ऐसी मशीन पर जो डेवलपर लैपटॉप नहीं है, npm ci का उपयोग करके इंस्टॉल करें, कभी भी npm install का नहीं:

npm ci --omit=dev --ignore-scripts

npm ci, npm install से उन तरीकों से अलग है जो यहाँ मायने रखते हैं। इसके लिए एक lockfile का होना आवश्यक है। यह शुरू होने से पहले किसी भी मौजूदा node_modules को हटा देता है, ताकि पिछले डिप्लॉय के अवशेष इस डिप्लॉय में न रह जाएँ। यह कभी भी package.json या lockfile में कुछ नहीं लिखता है, इसलिए इंस्टॉल प्रक्रिया आपको चुपचाप किसी नए वर्ज़न पर नहीं ले जा सकती। यदि lockfile और package.json में असहमति है, तो यह अंतर को हल करने के बजाय त्रुटि (error) के साथ बंद हो जाता है।

वह त्रुटि एक फीचर है, न कि कोई परेशानी। इसका मतलब है कि dependency में बदलाव किसी ऐसे कमिट के रूप में आना चाहिए जिसकी किसी ने समीक्षा की हो, न कि 02:00 बजे डिप्लॉयमेंट के साइड इफेक्ट के रूप में।

हर fetch पर इंटीग्रिटी हैश की जाँच की जाती है। जिस tarball के बाइट्स रिकॉर्ड किए गए हैश से मेल नहीं खाते, वह अनपैक होने के बजाय code EINTEGRITY के साथ इंस्टॉल में विफल हो जाता है। इससे आपको क्या मिलता है, इसके बारे में सटीक रहें: यह साबित करता है कि जो फाइल आपको मिली है, वह वही फाइल है जिसे lockfile ने पिन किया था, जो कि वही गारंटी है जो checksums के साथ डाउनलोड सत्यापित करना आपको देता है, और यह उसी तरह सीमित है। यह इस बारे में कुछ नहीं कहता कि क्या पिन किया गया वर्ज़न पब्लिश होते समय दुर्भावनापूर्ण (malicious) था।

--omit=dev के बारे में एक विवरण: उन पैकेजों को अभी भी रिज़ॉल्व किया जाता है और अभी भी lockfile में लिखा जाता है। उन्हें बस डिस्क पर नहीं रखा जाता है। डिस्क पर कम पैकेज होने का मतलब है कम इंस्टॉल स्क्रिप्ट और रनटाइम पर कम कोड लोड होना, इसलिए यह करना सार्थक है। यह आपके ट्री से किसी dependency को नहीं हटाता है।

इंस्टॉल स्क्रिप्ट्स को कोड की तरह मानें और उन्हें अस्वीकार करना सीखें

आप इंस्टॉल स्क्रिप्ट्स को बंद कर सकते हैं। इसे प्रोजेक्ट की .npmrc में डालें और इसे lockfile के साथ कमिट करें:

ignore-scripts=true
save-exact=true

ignore-scripts=true, npm को dependencies में घोषित स्क्रिप्ट्स चलाने से रोकता है। save-exact=true यह सुनिश्चित करता है कि npm install some-lib, 1.4.2 को ^1.4.2 के बजाय package.json में लिखे, ताकि कोई भी resolving range गलती से आपके मैनिफेस्ट में न आए।

यह कुछ चीजों को खराब कर सकता है, और इसे सक्षम करने से पहले आपको यह पता होना चाहिए कि ऐसा क्यों होता है। जो पैकेज native addon को कंपाइल करते हैं या prebuilt binary डाउनलोड करते हैं, वे यह काम इंस्टॉल स्क्रिप्ट में करते हैं। स्क्रिप्ट्स बंद होने पर, इंस्टॉल तो सफल हो जाता है, लेकिन विफलता बाद में runtime पर एक ऐसे मॉड्यूल के रूप में सामने आती है जो अपनी binding file लोड नहीं कर पाता। इसका समाधान एक allowlist है:

npm ci --ignore-scripts
npm rebuild better-sqlite3

npm rebuild <package> केवल उस एक पैकेज के लिए बिल्ड स्क्रिप्ट्स चलाता है। अब आपने हर पैकेज के लिए अलग से निर्णय लिया है, बजाय इसके कि आप उन सैकड़ों अनजान लोगों को बिना सोचे-समझे निष्पादन (execute) की अनुमति दे दें जिनसे आप कभी नहीं मिलेंगे।

यह देखने के लिए कि वर्तमान में यह अनुमति कितनी बड़ी है, npm से पूछें:

npm query ":attr(scripts, [postinstall])"

यह इंस्टॉल किए गए ट्री में उन सभी पैकेजों को प्रिंट करता है जिनमें postinstall स्क्रिप्ट होती है। एक सामान्य एप्लिकेशन पर यह सूची लोगों की अपेक्षा से छोटी होती है, और यही कारण है कि allowlist का उपयोग करना व्यावहारिक है।

बिल्ड प्रक्रिया को ट्रैफिक सर्व करने वाली प्रक्रिया से अलग करें

Deploy user को node_modules में लिखने की आवश्यकता होती है। HTTP requests का उत्तर देने वाली प्रक्रिया को इसकी आवश्यकता नहीं होती। यदि वे एक ही account हैं, तो install के दौरान चलने वाला code उन files को rewrite कर सकता है जो आपके users को serve की जाती हैं, और runtime पर चलने वाला code भी उन्हें rewrite कर सकता है।

उन्हें अलग करें। एक user के रूप में build करें, दूसरे के रूप में serve करें, और जिस directory से serve किया जा रहा है उसे serving account के लिए read-only बना दें:

sudo useradd --system --home-dir /srv/nodeapp --shell /usr/sbin/nologin nodeapp
sudo chown -R deploy:nodeapp /srv/nodeapp
sudo chmod -R o-rwx /srv/nodeapp

फिर systemd को इसे लागू करने दें। /etc/systemd/system/nodeapp.service लिखें:

[Unit]
Description=Node application
After=network-online.target

[Service]
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp/current
EnvironmentFile=/etc/nodeapp/env
ExecStart=/usr/bin/node server.js
Restart=on-failure

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/srv/nodeapp/shared
NoExecPaths=/srv/nodeapp/shared
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

[Install]
WantedBy=multi-user.target

ProtectSystem=strict इस service के लिए पूरी file system को read-only mount करता है, सिवाय /dev, /proc, /sys और उन directories के जिन्हें आप ReadWritePaths में सूचीबद्ध करते हैं। इसलिए application द्वारा node_modules में लिखने का कोई भी प्रयास EROFS: read-only file system के साथ विफल हो जाएगा, जिसे आप लगभग एक मिनट में अपने logs में देख और reproduce कर सकते हैं। NoExecPaths writable upload directory को कवर करता है: service वहाँ files लिख सकती है लेकिन kernel उन्हें execute करने से मना कर देता है। इस option के लिए systemd 249 या उससे नया version चाहिए, और Ubuntu 24.04 में 255 version आता है।

इस unit file में दो सावधानियां बरतें। पहला, MemoryDenyWriteExecute=yes को जोड़ें। यह अधिकांश systemd hardening सूचियों में दिखाई देता है, और यह Node को start होने से रोकता है, क्योंकि V8 runtime पर JavaScript को machine code में compile करता है और उसे ऐसे pages की आवश्यकता होती है जो writable और executable दोनों हों। दूसरा, command -v node से ExecStart path लें। यदि Node को version manager के साथ install किया गया था, तो यह deploy user की home directory के अंतर्गत रहता है, ProtectHome=yes फिर उस directory को service से छिपा देता है, और unit तुरंत status=203/EXEC के साथ विफल हो जाती है और log line में यह आता है कि executable नहीं मिल सका।

फाइल पर भरोसा करने के बजाय परिणाम की जाँच करें:

sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemd-analyze security nodeapp.service
sudo -u nodeapp touch /srv/nodeapp/current/probe

systemd-analyze security हर hardening setting को उसकी exposure के साथ सूचीबद्ध करता है, ताकि आप देख सकें कि कौन सी अभी भी default पर है। touch को Permission denied के साथ विफल होना चाहिए, क्योंकि nodeapp current के अंतर्गत किसी भी चीज़ का स्वामी नहीं है। यदि यह सफल होता है, तो आपकी file ownership गलत है और systemd settings चुपचाप उसे छिपा रही हैं।

EnvironmentFile पर एक नोट: systemd इसे root के रूप में पढ़ता है इससे पहले कि वह User=nodeapp पर switch करे, इसलिए वह file mode 600 के साथ root:root हो सकती है। Application को फिर भी variables प्राप्त होते हैं। nodeapp के रूप में shell access रखने वाला कोई भी व्यक्ति उन्हें /proc/<pid>/environ से पढ़ सकता है, इसलिए यह secret को at rest सुरक्षित रखता है, न कि running process को।

Deploy credentials को build environment से बाहर रखें

Install scripts environment को inherit करते हैं। यह एक तथ्य ही यह तय करने के लिए पर्याप्त है कि आपको build कहाँ करना चाहिए।

सबसे सुरक्षित तरीका यह है कि आप production server के अलावा किसी अन्य स्थान पर build करें और तैयार directory को वहाँ copy कर लें। उस स्थिति में build machine के पास केवल एक read-only registry token होता है और कुछ नहीं। न कोई SSH deploy key, न cloud access key, न database password, और न ही container registry login।

npm token create --read-only

एक read-only token केवल packages fetch कर सकता है, उन्हें publish नहीं कर सकता। यदि इसे build environment से चुरा भी लिया जाए, तो नुकसान केवल public packages को download करने की क्षमता तक ही सीमित रहता है।

यदि आपको server पर ही build करना अनिवार्य है, तो deploy user के रूप में build करें जिसका environment जानबूझकर सीमित रखा गया हो, और runtime secrets को /etc/nodeapp/env में रखें, जिसे deploy पढ़ न सके। यही तर्क आपके द्वारा host की गई build automation पर भी लागू होता है: एक self-hosted GitHub Actions runner tokens रखता है और हर job पर मनमाना published code execute करता है, जो इसे एक छोटे deployment में सबसे अधिक मूल्यवान machine बना देता है। कोई भी ऐसा program जिसे आपने नहीं लिखा है और जो आपका पूरा environment प्राप्त करता है, वह इसी श्रेणी में आता है, और यही कारण है कि AI agent के environment से secrets को बाहर रखना इसी समस्या का एक और रूप है जिसमें बीच में एक अलग program होता है।

जिसे आप ऑडिट नहीं कर सकते उसे पिन करें या वेंडर करें

पिन की गई dependency वह है जिसका version बिना commit के नहीं बदल सकता। committed lockfile पूरे tree के लिए पहले से ही ऐसा करता है। दो स्थितियों में और अधिक करने की आवश्यकता होती है।

Transitive dependencies पहली स्थिति है। आप यह नियंत्रित नहीं करते कि आपकी dependencies किस पर निर्भर हैं। overrides, package.json में tree में कहीं भी एक version को बाध्य (force) करता है:

{
  "overrides": {
    "some-transitive-lib": "1.4.2"
  }
}

इसे जोड़ने के बाद एक बार npm install चलाएं ताकि lockfile परिणाम को record कर ले, फिर दोनों फाइलों को commit करें।

दूसरी स्थिति वह package है जिसे आप न तो ऑडिट कर सकते हैं और न ही हटा सकते हैं। उसे वेंडर (vendor) करें। npm pack उस सटीक tarball को डाउनलोड करता है जिसे registry serve करेगी, और एक file: dependency आपकी कॉपी से install होती है:

npm pack some-lib@1.4.2
mkdir -p vendor && mv some-lib-1.4.2.tgz vendor/
{
  "dependencies": {
    "some-lib": "file:vendor/some-lib-1.4.2.tgz"
  }
}

Tarball अब आपके repository में रहता है और आपके बिना बदले नहीं बदल सकता। आपने हमेशा के लिए इसके updates की जिम्मेदारी भी ले ली है, इसलिए इसका उपयोग उस छोटे abandoned package के लिए करें जिसके साथ आप फंस गए हैं, न कि अपने web framework के लिए।

एक कूलिंग-ऑफ अवधि भी होती है, जिसकी कोई लागत नहीं है:

npm install --before=2026-08-01

before विकल्प केवल उन versions का उपयोग करके tree को फिर से बनाता है जो उस तारीख को या उससे पहले प्रकाशित किए गए थे। जब आप dependencies को refresh करते हैं, तो इसे एक या दो सप्ताह पीछे सेट करें, और आप उस समय-सीमा से बच जाते हैं जिसमें एक खराब release live होती है और अभी तक रिपोर्ट नहीं की गई होती है। यह एक स्थूल उपकरण है, क्योंकि यह वास्तविक सुरक्षा सुधारों (security fixes) को भी रोक देता है। ranges को हल करने के लिए इसका उपयोग करें, पढ़ें कि क्या बदला है, फिर lockfile को commit करें।

मुझे कैसे पता चलेगा कि मैंने वास्तव में कौन सा version ship किया है?

Git में मौजूद lockfile यह बताती है कि क्या install होना चाहिए था। डिस्क यह बताती है कि क्या install हुआ है। केवल दूसरी वाली ही प्रमाण है।

npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"

npm ls, node_modules को पढ़ता है, इसलिए यह lockfile के इरादे के बजाय भौतिक रूप से मौजूद चीज़ की रिपोर्ट करता है। node -e लाइन install किए गए manifest को path के अनुसार पढ़ती है, जो उन packages के लिए भी काम करती है जिनके exports field subpath imports को रोकते हैं, और यह बिना किसी tree drawing के एक version print करती है।

तुलना के दूसरे भाग के लिए, git को पढ़ें:

git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'

commit को deploy layout में डालकर दोनों के बीच स्थायी संबंध बनाएँ। /srv/nodeapp/releases/<short commit sha> में release करें और एक symlink के साथ /srv/nodeapp/current को उस पर point करें। "अभी क्या चल रहा है" का उत्तर readlink /srv/nodeapp/current बन जाता है, और यह 03:00 बजे उस व्यक्ति के लिए भी उपलब्ध होता है जिसने इसे deploy नहीं किया था।

अंत में, जाँचें कि registry किस बात की पुष्टि करेगी:

npm audit signatures

यह आपके installed tree में मौजूद packages पर registry signatures को verify करता है, और जिन packages में provenance attestations हैं, उनके लिए उन्हें भी verify करता है। Provenance एक प्रकाशित tarball को उस public continuous integration (CI) build से जोड़ता है जिसने उसे तैयार किया है, इसलिए एक verified attestation का मतलब है कि आप code को किसी अज्ञात laptop के बजाय एक commit तक trace कर सकते हैं। Coverage सार्वभौमिक नहीं है, इसलिए किसी missing attestation को "कोई जानकारी नहीं" के रूप में पढ़ें, न कि "खराब package" के रूप में।

जब आपके सर्वर पर कोई खराब release पहुँच जाए तो क्या करें

जो चला है और जिस user के रूप में चला है, उससे बाहर की ओर काम करें।

यदि code install के दौरान चला था, तो मान लें कि build user द्वारा पढ़ी जा सकने वाली हर चीज़ अब सुरक्षित नहीं है। registry token, उस home directory में मौजूद SSH keys, cloud credentials और shell में export किए गए किसी भी secret को rotate करें। Rotation ही एकमात्र ईमानदार प्रतिक्रिया है, क्योंकि आप यह साबित नहीं कर सकते कि कोई फ़ाइल पढ़ी नहीं गई थी।

यदि code runtime पर एक locked-down service account के तहत चला था, तो पहुँच योग्य दायरा बहुत छोटा होता है: केवल application के अपने environment variables और वह सब कुछ जहाँ तक उसका network access पहुँच सकता है। VPS पर unprivileged users के रूप में services चलाने का पूरा तर्क यही है। यह compromise को रोकता नहीं है। यह तय करता है कि compromise को मशीन का कितना हिस्सा मिलता है, और क्या वह restart के बाद भी बना रहता है।

फिर सफाई करने के बजाय rebuild करें। node_modules को delete करें, प्रभावित package को package.json में खराब version से नीचे pin करें, lockfile को update करने के लिए एक बार npm install चलाएँ, उसे commit करें और npm ci के साथ deploy करें। tree को उसी स्थान पर repair न करें। आप यह नहीं बता सकते कि install script ने किन चीज़ों को छुआ है।

उस समय-सीमा को भी लिख लें: पहला deploy जिसने उस version को pull किया हो सकता है, और वह deploy जिसने उसे हटा दिया। वह दायरा आपको बताता है कि आपको अपने कौन से logs पढ़ने हैं, और इसका उत्तर तभी मिल सकता है जब आपके releases के नाम commits के आधार पर रखे गए हों।

इनमें से कोई भी उपाय क्या ठीक नहीं करता है

Lockfile किसी dependency को सुरक्षित नहीं बनाता है। यह उस क्षण को, जब आपने उस dependency को स्वीकार किया था, एक deploy के side effect के बजाय एक पुरानी, जांची-परखी प्रक्रिया में बदल देता है। ऊपर बताई गई हर प्रक्रिया इसी रूपांतरण को अंजाम देती है, यानी दुर्घटना को एक विकल्प में बदलती है।

npm audit यहाँ कोई सुरक्षा कवच नहीं है। यह आपके tree की तुलना रिपोर्ट की गई vulnerabilities के डेटाबेस से करता है, इसलिए यह केवल उन समस्याओं को ढूंढता है जो पहले से प्रकाशित और ज्ञात हैं। Supply-chain attack अपने पूरे उपयोगी जीवनकाल के दौरान अज्ञात रहता है। पुराने ज्ञात bugs के लिए npm audit चलाएं, लेकिन चार घंटे पहले release हुए किसी package के बारे में इससे किसी मदद की उम्मीद न रखें।

अपनी dependency की संख्या कम करना इस गाइड के किसी भी अन्य टूल से अधिक मदद करता है, और यह सबसे कम लोकप्रिय सलाह है। जिस हर package को आप नहीं जोड़ते हैं, उसका मतलब है कि एक और publisher जिसे आपकी ओर से phish नहीं किया जा सकता, और एक और install script जो आपके deploy user के रूप में कभी नहीं चलती है।

इनमें से कुछ भी केवल npm तक सीमित नहीं है। यही चार सिद्धांत PyPI, RubyGems, container images और आपके distribution के package manager पर भी लागू होते हैं। npm में यह सबसे अधिक दिखाई देता है क्योंकि यहाँ trees सबसे गहरी होती हैं और install scripts डिफ़ॉल्ट रूप से चलती हैं। आसपास की मशीन का कितना हिस्सा आपकी सुरक्षा के दायरे में है, यह इस पर निर्भर करता है कि वह कहाँ चल रही है, जो कि क्या VPS hosting सुरक्षित है, इस व्यापक प्रश्न का एक हिस्सा है।

FAQ

क्या npm ci मुझे किसी compromised npm package से बचाता है?

यह आपको आपकी जानकारी के बिना version बदलने से बचाता है। npm ci ठीक वही install करता है जो package-lock.json रिकॉर्ड करता है, प्रत्येक tarball को उसके sha512 integrity hash के विरुद्ध जाँचता है, और यदि package.json और lockfile में असहमति हो तो अंतर को हल करने के बजाय error के साथ exit हो जाता है। यह इस बारे में कुछ नहीं कहता कि pinned version सुरक्षित है या नहीं। यदि आप कोई ऐसा lockfile commit करते हैं जो किसी malicious version को pin करता है, तो npm ci हर बार आपके हर सर्वर पर उस version को निष्ठापूर्वक install कर देगा।

क्या मुझे हर चीज़ के लिए ignore-scripts=true set करना चाहिए?

इसे set करें, फिर allowlist का उपयोग करें। प्रोजेक्ट के .npmrc में ignore-scripts=true dependency install scripts को चलने से रोकता है, जो एक खराब package से आपके deploy user के credentials तक पहुँचने का सबसे सीधा रास्ता बंद कर देता है। जो packages native addon compile करते हैं या prebuilt binary fetch करते हैं, उन्हें वास्तव में इनकी आवश्यकता होती है, और scripts बंद होने पर वे install के समय के बजाय बाद में runtime पर missing binding file के साथ fail हो जाते हैं। npm ci --ignore-scripts चलाएँ, फिर उन कुछ packages के लिए npm rebuild <package> चलाएँ जिन पर आपने भरोसा करने का निर्णय लिया है। npm query ":attr(scripts, [postinstall])" दिखाता है कि वास्तव में ऐसे कितने packages हैं।

मुझे कैसे पता चलेगा कि मेरे सर्वर ने वास्तव में package का कौन सा version install किया है?

Lockfile के बजाय disk को पढ़ें। npm ls <package> रिपोर्ट करता है कि node_modules में क्या मौजूद है, और node -e "console.log(require('./node_modules/<package>/package.json').version)" केवल version string प्रिंट करता है। Git में मौजूद lockfile एक अलग सवाल का जवाब देता है, जो यह है कि क्या install होना चाहिए था, और इन दोनों की तुलना करना ही मुख्य उद्देश्य है। Git commit के नाम पर रखे गए directory में deploy करने से महीनों बाद भी दोनों जवाब उपलब्ध रहते हैं, जब आपको उनकी आवश्यकता होती है।

क्या npm audit supply-chain attacks को ढूँढता है?

नहीं। npm audit आपके tree का मिलान रिपोर्ट की गई vulnerabilities के database से करता है, इसलिए यह केवल उन मुद्दों को ढूँढता है जो पहले ही प्रकाशित हो चुके हैं और जिन्हें identifier दिया गया है। एक malicious release उन घंटों या दिनों के दौरान unreported रहता है जब उसे install करना मायने रखता है। npm audit signatures अधिक उपयोगी command है: यह आपके installed tree में registry signatures को verify करता है और provenance attestations की जाँच करता है जहाँ publisher ने उन्हें तैयार किया है, जो आपको बताता है कि tarball किसी अज्ञात machine के बजाय public build से आया है।

यदि हमला install के समय होता है, तो app को unprivileged user के रूप में चलाना क्यों मायने रखता है?

क्योंकि दोनों विफलताओं की पहुँच अलग-अलग होती है और आप दोनों के विरुद्ध बचाव कर रहे होते हैं। Install के समय चलने वाला code deploy user के रूप में चलता है और उस user की SSH keys, registry tokens और cloud credentials को पढ़ सकता है। Runtime code service account के रूप में चलता है, और User=nodeapp, ProtectSystem=strict के साथ और disk पर कोई credentials न होने के कारण, इसकी पहुँच केवल application के अपने environment और उसके database तक सीमित रहती है। Accounts को अलग करने का मतलब यह भी है कि traffic serve करने वाली process node_modules को rewrite नहीं कर सकती, इसलिए runtime compromise अगले restart पर समाप्त हो जाता है, न कि स्थायी बन जाता है।