SSD Nodes Learn 🎉 VPS $5.50/ماہ سے
تعلیمی Matt Connorتحریر: Matt Connor

npm supply-chain حملہ اور Node.js سرور کی حفاظت

npm supply-chain حملے آپ کے Node.js سرور تک کیسے پہنچتے ہیں؟ malicious patches، postinstall اسکرپٹس اور typosquats سے بچنے کے لیے اپنی deploy حکمت عملی کو محفوظ بنائیں۔

npm supply-chain حملہ کیا ہوتا ہے

npm supply-chain حملہ آپ کے سرور تک اس پیکیج کے ذریعے پہنچتا ہے جسے آپ نے انسٹال کرنے کا انتخاب کیا ہے۔ اس میں کوئی open port ملوث نہیں ہوتی اور نہ ہی exploit کا کوئی مرحلہ ہوتا ہے۔ npm (node package manager) کوڈ انسٹال کرتا ہے، اور کوڈ انسٹال کرنے کا مطلب کوڈ کو چلانا ہے، لہذا ایک چھوٹی سی Node ایپلی کیشن سینکڑوں ایسے پیکیجز کھینچ لیتی ہے جنہیں آپ نے کبھی نہیں پڑھا، اور ان میں سے کوئی بھی ایک گھنٹے بعد نیا ورژن شائع کر سکتا ہے۔

آپ کی deploy ایک نقصان دہ ورژن حاصل کر لیتی ہے کیونکہ آپ کی install کمانڈ نے تازہ ترین مماثل ورژن مانگا ہوتا ہے۔ وہ کوڈ پھر اس شخص کے حقوق (privileges) کے ساتھ چلتا ہے جس نے انسٹالیشن چلائی تھی۔ نیچے دی گئی ہر چیز انہی دو جملوں سے اخذ ہوتی ہے۔

یہ اشکال اس ترتیب میں ہیں کہ وہ کتنی بار ایک شخص کو متاثر کرتی ہیں جو ایک Node ایپ کو ایک VPS پر deploy کر رہا ہو۔ یہ ترتیب وہ نہیں ہے جو ایک بڑی کمپنی استعمال کرے گی، کیونکہ ایک بڑی کمپنی کے پاس internal registry، ایک review ٹیم اور public registry کا ایک mirror ہوتا ہے۔ آپ کے پاس صرف ایک deploy اسکرپٹ ہے۔

شکل 1: ایک مینٹینر اکاؤنٹ کا ہیک ہونا اور پیچ جاری کرنا

npm رجسٹری کسی کو بھی پہلے سے موجود ورژن کے مواد کو تبدیل کرنے کی اجازت نہیں دیتی۔ لہذا، ایک حملہ آور جو کسی مینٹینر کو فشنگ کا شکار بناتا ہے یا پبلش ٹوکن چوری کرتا ہے، وہ 4.18.2 کو دوبارہ نہیں لکھ سکتا۔ وہ 4.18.3 شائع کرتے ہیں۔

اپنی package.json کو دیکھیں۔ "express": "^4.18.2" جیسی لائن کا مطلب ورژن 4.18.2 نہیں ہے۔ کیریٹ (caret) کا مطلب ہے "4.x کا کوئی بھی ورژن جو اس کے برابر یا اس سے اوپر ہو"، اور ~4.18.2 کا مطلب ہے "کوئی بھی 4.18.x ورژن"۔ npm install اس رینج کو چلنے کے وقت حل (resolve) کرتا ہے، لہذا ایک ہی git کمٹ، جسے ایک ہی سہ پہر میں دو بار ڈیپلائے کیا جائے، کوڈ کے دو مختلف سیٹ انسٹال کر سکتا ہے۔ یہ خلا ہی حملہ کرنے کی سطح (attack surface) ہے۔ اسے کھولنے کے لیے آپ کی مشین پر کسی چیز کا ہیک ہونا ضروری نہیں ہے۔

مضر ریلیزز کی عام طور پر اطلاع دی جاتی ہے اور انہیں ہٹا دیا جاتا ہے، لیکن یہ ہٹانا لوگوں کے انسٹال کرنے کے بعد ہوتا ہے۔ جس نے بھی اس ونڈو کے دوران ڈیپلائے کیا، اس کے پاس ڈسک پر وہ کوڈ موجود ہوتا ہے۔ ایک پائپ لائن جو ہر رن پر رینجز کو حل کرتی ہے، وہ خود بخود اس ونڈو میں داخل ہو جاتی ہے، ہفتے میں کئی بار، بغیر کسی کے فیصلہ کیے۔

شکل 2: ایک انسٹال اسکرپٹ اس صارف کے طور پر چلتا ہے جو ڈیپلائے کر رہا ہے

ایک پیکیج کا package.json اپنے scripts بلاک میں preinstall، install، postinstall اور prepare کا اعلان کر سکتا ہے۔ npm انہیں انسٹالیشن کے دوران چلاتا ہے۔ یہ سینڈ باکسڈ (sandboxed) نہیں ہوتے اور کوئی بھی ان کا جائزہ نہیں لیتا۔ یہ شیل کمانڈز ہوتی ہیں جو اس صارف کے طور پر چلتی ہیں جس نے انسٹال کمانڈ ٹائپ کی ہے، اسی صارف کی ہوم ڈائریکٹری میں، اسی صارف کی نیٹ ورک رسائی کے ساتھ اور اس شیل کے مکمل ماحول (environment) کے ساتھ۔

لہذا، مفید سوال یہ نہیں ہے کہ پیکیج کیا کر سکتا ہے۔ بلکہ یہ ہے کہ وہ صارف کیا پڑھ سکتا ہے۔ ایک عام ڈیپلائے باکس پر اس کا جواب یہ ہے کہ اس میں ~/.npmrc شامل ہے جس میں رجسٹری ٹوکن ہوتا ہے، ~/.ssh/id_ed25519 جو SSH (سیکیور شیل) کے لیے ڈیپلائے کی کے طور پر استعمال ہوتا ہے، ~/.aws/credentials، ~/.docker/config.json، اور شیل میں موجود ہر ایکسپورٹ شدہ ویری ایبل، جہاں عام طور پر DATABASE_URL موجود ہوتا ہے۔

اس طرح کے پے لوڈ (payload) کو نہ تو استقامت (persistence) کی ضرورت ہوتی ہے اور نہ ہی مراعات میں اضافے (privilege escalation) کی۔ یہ چند فائلیں پڑھتا ہے، انہیں HTTPS کے ذریعے ایک ہوسٹ پر بھیجتا ہے، اور 0 اسٹیٹس کے ساتھ ایگزٹ ہو جاتا ہے۔ آپ کو کچھ نظر نہیں آتا، کیونکہ npm بائی ڈیفالٹ انسٹال اسکرپٹ کے آؤٹ پٹ کو چھپا دیتا ہے۔ اسے بند کریں اور دیکھیں کہ اصل میں کیا چل رہا ہے:

npm ci --foreground-scripts

foreground-scripts، npm پروسیس کے ساتھ اسٹینڈرڈ ان پٹ، آؤٹ پٹ اور ایرر شیئر کرتا ہے، لہذا بلڈ اسکرپٹس آپ کے ٹرمینل میں پرنٹ ہوتی ہیں، نہ کہ کسی ایسے بفر میں جسے npm انسٹال کامیاب ہونے پر ضائع کر دیتا ہے۔

شکل 3: ٹائپو سکواٹس، اور وہ نام جو آپ نے ٹھیک سے ٹائپ نہیں کیا

ٹائپو سکواٹ (typosquat) ایک ایسا پیکیج ہے جو کسی مقبول پیکیج کے ملتے جلتے نام سے شائع کیا جاتا ہے، تاکہ غلط ٹائپنگ یا غلط پیسٹ کیے گئے انسٹال کمانڈ کا فائدہ اٹھایا جا سکے۔ اس کا طریقہ کار کمانڈ ہے، کوڈ نہیں، اس لیے یہاں lockfile آپ کی مدد نہیں کرتی: آپ ایک بار غلط نام شامل کر لیتے ہیں، اور اس کے بعد lockfile وفاداری سے اسے پن (pin) کر دیتی ہے۔

جو ویریئنٹ افراد کے بجائے ٹیموں کو نشانہ بناتا ہے وہ dependency confusion ہے۔ آپ کا اندرونی پیکیج billing-utils کہلاتا ہے اور ایک نجی رجسٹری پر موجود ہے۔ اگر عوامی رجسٹری پر billing-utils نام کی کوئی چیز موجود نہ ہو، تو کوئی بھی اسے شائع کر سکتا ہے۔ npm غیر اسکوپڈ (unscoped) ناموں کو ڈیفالٹ عوامی رجسٹری کے خلاف حل کرتا ہے، لہذا عوامی کاپی جیت سکتی ہے۔ اس کا حل ایک ایسا اسکوپ ہے جس کے آپ مالک ہوں، اور اس اسکوپ کے لیے رجسٹری میپنگ، جو .npmrc میں درج ہو:

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

اب @yourorg/billing-utils صرف اسی ہوسٹ سے حاصل کیا جاتا ہے، کیونکہ اسکوپ ٹو رجسٹری میپنگ سے ڈیفالٹ رجسٹری سے پہلے مشورہ کیا جاتا ہے۔ ایک غیر اسکوپڈ اندرونی نام کی کوئی میپنگ نہیں ہوتی، اس لیے اس کی کوئی حفاظت نہیں ہوتی۔

کسی بھی نئی ڈیپینڈنسی کو شامل کرنے سے پہلے، اس کے ڈاؤن لوڈ بیج کے بجائے خود پیکیج کو دیکھیں:

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

پچھلے مہینے بنایا گیا پیکیج، جسے ایسے اکاؤنٹ نے شائع کیا ہو جس کا تعلق آپ کسی عوامی ریپوزٹری سے نہیں جوڑ سکتے، چھ سالہ تاریخ رکھنے والے پیکیج سے مختلف خطرہ ہے۔ ان میں سے کوئی بھی حقیقت ثبوت نہیں ہے۔ دونوں کی جانچ کرنا آسان ہے۔

شکل 4: وہ انحصار جس کا مالک خاموشی سے بدل گیا

Maintainers پیکیجز دوسروں کے حوالے کر دیتے ہیں۔ کوئی تھک ہار کر کام چھوڑ دیتا ہے، کوئی اجنبی مدد کی پیشکش کرتا ہے، اشاعت کے حقوق منتقل ہو جاتے ہیں، اور اس پر انحصار کرنے والے پروجیکٹس تک کسی قسم کی اطلاع نہیں پہنچتی۔ کوئی چیز compromised نہیں ہوتی۔ جو اعتماد آپ نے 2021 میں کیا تھا، اب وہ کسی اور شخص کے پاس ہے۔

یہ سب سے سست شکل ہے اور اس کا پتہ لگانا سب سے مشکل ہے، اور کوئی بھی کمانڈ براہ راست اس کا جواب نہیں دیتی۔ دو چیزیں اس کے دائرہ کار کو محدود کرتی ہیں۔ کسی پیکیج کو اپنانے سے پہلے چیک کریں کہ کون اسے 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

پہلی شکل صرف تبدیل شدہ فائل کے نام پرنٹ کرتی ہے، جو کہ آپ کے لیے اہم پیکیج کی ہر اپ گریڈ پر چیک کرنے کے لیے کافی تیز ہے۔ ایک patch release جو build script کو چھوئے، پیکیج روٹ میں کوئی فائل شامل کرے، یا scripts بلاک میں ترمیم کرے، اسے آپ کے سرور تک پہنچنے سے پہلے مکمل پڑھنا ضروری ہے۔

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 میں اختلاف ہو، تو یہ فرق کو حل کرنے کے بجائے ایرر کے ساتھ بند ہو جاتا ہے۔

وہ ایرر ایک فیچر ہے، نہ کہ پریشانی۔ اس کا مطلب ہے کہ ڈیپینڈنسی میں تبدیلی کسی ایسے کمٹ کے ذریعے آنی چاہیے جس کا کسی نے جائزہ لیا ہو، نہ کہ رات 02:00 بجے ڈیپلائے کے ضمنی اثر کے طور پر۔

ہر فیچ (fetch) پر انٹیگریٹی ہیش چیک کیا جاتا ہے۔ وہ tarball جس کے بائٹس ریکارڈ شدہ ہیش سے میل نہیں کھاتے، وہ ان پیک ہونے کے بجائے code EINTEGRITY کے ساتھ انسٹالیشن کو ناکام بنا دیتا ہے۔ اس کے فوائد کے بارے میں درست رہیں: یہ ثابت کرتا ہے کہ جو فائل آپ کو موصول ہوئی ہے وہ وہی فائل ہے جسے lockfile نے پن (pin) کیا تھا، جو کہ وہی گارنٹی ہے جو checksums کے ساتھ ڈاؤن لوڈز کی تصدیق آپ کو دیتی ہے، اور یہ اسی طرح محدود ہے۔ یہ اس بارے میں کچھ نہیں کہتا کہ آیا پن کیا گیا ورژن شائع ہونے کے وقت نقصان دہ تھا یا نہیں۔

--omit=dev کے بارے میں ایک تفصیل: وہ پیکیجز اب بھی ریزولو ہوتے ہیں اور اب بھی lockfile میں لکھے جاتے ہیں۔ انہیں صرف ڈسک پر نہیں رکھا جاتا۔ ڈسک پر کم پیکیجز کا مطلب ہے کم انسٹال اسکرپٹس اور رن ٹائم پر کم کوڈ لوڈ ہونا، لہذا یہ کرنا فائدہ مند ہے۔ یہ آپ کے ٹری سے کسی ڈیپینڈنسی کو نہیں ہٹاتا۔

انسٹال اسکرپٹس کو کوڈ سمجھیں اور انہیں مسترد کرنا سیکھیں

آپ انسٹال اسکرپٹس کو بند کر سکتے ہیں۔ اسے پروجیکٹ کی .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 فائل لوڈ نہیں کر پاتا۔ اس کا حل ایک allowlist ہے:

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

npm rebuild <package> صرف اس ایک پیکج کے لیے build اسکرپٹس چلاتا ہے۔ اب آپ نے ہر پیکج کے لیے الگ فیصلہ کیا ہے، بجائے اس کے کہ آپ سینکڑوں اجنبیوں کو، جنہیں آپ کبھی نہیں ملیں گے، بلا تفریق execute کرنے کی اجازت دے دیں۔

یہ دیکھنے کے لیے کہ یہ اجازت فی الحال کتنی وسیع ہے، npm سے پوچھیں:

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

یہ انسٹال شدہ ٹری میں موجود ہر اس پیکج کو پرنٹ کرتا ہے جس میں postinstall اسکرپٹ موجود ہو۔ ایک عام ایپلی کیشن پر یہ فہرست لوگوں کی توقع سے چھوٹی ہوتی ہے، اور یہی وہ چیز ہے جو allowlist کو عملی بناتی ہے۔

بلڈ (build) کو ٹریفک سرو کرنے والے عمل سے الگ کریں

deploy صارف کو node_modules میں لکھنے کی ضرورت ہوتی ہے۔ HTTP درخواستوں کا جواب دینے والے عمل کو اس کی ضرورت نہیں ہوتی۔ اگر یہ دونوں ایک ہی اکاؤنٹ ہوں، تو انسٹالیشن کے دوران چلنے والا کوڈ ان فائلوں کو دوبارہ لکھ سکتا ہے جو آپ کے صارفین کو سرو کی جا رہی ہیں، اور رن ٹائم پر چلنے والا کوڈ بھی ایسا کر سکتا ہے۔

انہیں الگ کریں۔ ایک صارف کے طور پر بلڈ کریں، دوسرے کے طور پر سرو کریں، اور سرو کرنے والے اکاؤنٹ کے لیے ڈائریکٹری کو صرف پڑھنے کے قابل (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 اس سروس کے لیے پورے فائل سسٹم کو read-only بنا دیتا ہے، سوائے /dev، /proc، /sys اور ان ڈائریکٹریز کے جنہیں آپ ReadWritePaths میں درج کرتے ہیں۔ لہذا اگر ایپلیکیشن node_modules میں لکھنے کی کوشش کرے گی تو وہ EROFS: read-only file system کے ساتھ ناکام ہو جائے گی، جسے آپ ایک منٹ کے اندر اپنے لاگز میں جا کر دیکھ سکتے ہیں۔ NoExecPaths اپ لوڈ والی writable ڈائریکٹری کا احاطہ کرتا ہے: سروس وہاں فائلیں لکھ سکتی ہے لیکن کرنل انہیں execute کرنے سے انکار کر دیتا ہے۔ اس آپشن کے لیے systemd 249 یا اس سے نیا ورژن درکار ہے، اور Ubuntu 24.04 میں 255 ورژن موجود ہے۔

اس یونٹ فائل میں دو جال ہیں۔ پہلا، MemoryDenyWriteExecute=yes کو شامل نہ کریں۔ یہ زیادہ تر systemd ہارڈننگ لسٹوں میں نظر آتا ہے، اور یہ Node کو شروع ہونے سے روک دیتا ہے، کیونکہ V8 رن ٹائم پر JavaScript کو مشین کوڈ میں کمپائل کرتا ہے اور اسے ایسی صفحات کی ضرورت ہوتی ہے جو writable اور executable دونوں ہوں۔ دوسرا، command -v node سے ExecStart کا پاتھ لیں۔ اگر Node کو کسی ورژن مینیجر کے ساتھ انسٹال کیا گیا تھا تو یہ deploy صارف کی ہوم ڈائریکٹری میں ہوتا ہے، ProtectHome=yes پھر اس ڈائریکٹری کو سروس سے چھپا دیتا ہے، اور یونٹ فوری طور پر status=203/EXEC کے ساتھ ناکام ہو جاتا ہے اور لاگ لائن میں یہ پیغام آتا ہے کہ 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 ہر ہارڈننگ سیٹنگ کو اس کے ایکسپوزر کے ساتھ لسٹ کرتا ہے، تاکہ آپ دیکھ سکیں کہ کون سی سیٹنگز ابھی بھی ڈیفالٹ پر ہیں۔ touch کو Permission denied کے ساتھ ناکام ہونا چاہیے، کیونکہ nodeapp کی current کے نیچے کوئی ملکیت نہیں ہے۔ اگر یہ کامیاب ہو جاتا ہے، تو آپ کی فائل اونرشپ غلط ہے اور systemd کی سیٹنگز خاموشی سے اس کی تلافی کر رہی ہیں۔

EnvironmentFile کے بارے میں ایک نوٹ: systemd اسے root کے طور پر پڑھتا ہے اس سے پہلے کہ وہ User=nodeapp پر منتقل ہو، لہذا وہ فائل root:root ہو سکتی ہے جس کا موڈ 600 ہو۔ ایپلیکیشن کو پھر بھی ویری ایبلز موصول ہو جاتے ہیں۔ کوئی بھی شخص جس کے پاس nodeapp کے طور پر شیل ہو، وہ اب بھی انہیں /proc/<pid>/environ سے پڑھ سکتا ہے، لہذا یہ سیکریٹ کو صرف اسٹوریج (at rest) پر محفوظ کرتا ہے، چلتے ہوئے عمل (running process) پر نہیں۔

ڈپلائے کریڈینشلز کو بلڈ انوائرمنٹ سے باہر رکھیں

انسٹال اسکرپٹس انوائرمنٹ کو وراثت میں حاصل کرتی ہیں۔ یہ ایک حقیقت اس بات کا فیصلہ کرنے کے لیے کافی ہونی چاہیے کہ آپ کہاں بلڈ کرتے ہیں۔

سب سے محفوظ طریقہ یہ ہے کہ ایسی جگہ بلڈ کریں جو پروڈکشن سرور نہ ہو اور تیار شدہ ڈائریکٹری کو وہاں کاپی کر لیں۔ بلڈ مشین کے پاس صرف ایک read-only رجسٹری ٹوکن ہونا چاہیے اور کچھ نہیں۔ کوئی SSH ڈپلائے کی، کوئی کلاؤڈ ایکسیس کی، کوئی ڈیٹا بیس پاس ورڈ، اور نہ ہی کنٹینر رجسٹری لاگ ان۔

npm token create --read-only

ایک read-only ٹوکن صرف پیکیجز ڈاؤن لوڈ کر سکتا ہے، پبلش نہیں کر سکتا۔ اگر یہ کسی بلڈ انوائرمنٹ سے چوری ہو جائے، تو نقصان صرف پبلک پیکیجز ڈاؤن لوڈ کرنے کی صلاحیت تک محدود رہتا ہے۔

اگر آپ کو سرور پر ہی بلڈ کرنا پڑے، تو deploy صارف کے طور پر بلڈ کریں جس کا انوائرمنٹ جان بوجھ کر محدود رکھا گیا ہو، اور رن ٹائم سیکریٹس کو /etc/nodeapp/env میں رکھیں، جسے deploy پڑھ نہ سکے۔ یہی منطق آپ کی اپنی ہوسٹ کردہ بلڈ آٹومیشن پر بھی لاگو ہوتی ہے: ایک سیلف ہوسٹڈ GitHub Actions رنر ٹوکنز رکھتا ہے اور ہر جاب پر من مانا پبلش شدہ کوڈ چلاتا ہے، جو اسے ایک چھوٹی ڈپلائمنٹ میں سب سے زیادہ قیمتی مشین بنا دیتا ہے۔ کوئی بھی ایسا پروگرام جسے آپ نے نہیں لکھا اور جو آپ کا پورا انوائرمنٹ حاصل کرتا ہے، اسی زمرے میں آتا ہے، اور یہی وجہ ہے کہ AI ایجنٹ کے انوائرمنٹ سے سیکریٹس کو دور رکھنا دراصل یہی مسئلہ ہے جس میں درمیان میں ایک مختلف پروگرام موجود ہے۔

جسے آپ آڈٹ نہیں کر سکتے اسے پن (pin) کریں یا وینڈر (vendor) کریں

ایک پن شدہ (pinned) ڈیپینڈنسی وہ ہے جس کا ورژن کسی کمٹ (commit) کے بغیر تبدیل نہیں ہو سکتا۔ کمٹ شدہ لاک فائل (lockfile) پورے ٹری (tree) کے لیے پہلے ہی یہ کام کرتی ہے۔ دو صورتوں میں مزید اقدامات کی ضرورت ہوتی ہے۔

پہلی صورت ٹرانزیٹو ڈیپینڈنسیز (transitive dependencies) کی ہے۔ آپ اس بات کو کنٹرول نہیں کرتے کہ آپ کی ڈیپینڈنسیز کن چیزوں پر انحصار کرتی ہیں۔ overrides میں package.json ٹری میں کہیں بھی ایک ورژن کو زبردستی نافذ کرتا ہے:

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

اسے شامل کرنے کے بعد ایک بار npm install چلائیں تاکہ لاک فائل نتیجے کو ریکارڈ کر سکے، پھر دونوں فائلوں کو کمٹ کر دیں۔

دوسری صورت ایک ایسے پیکیج کی ہے جسے آپ آڈٹ نہیں کر سکتے اور نہ ہی اسے ہٹا سکتے ہیں۔ اسے وینڈر (vendor) کر لیں۔ npm pack بالکل وہی ٹار بال (tarball) ڈاؤن لوڈ کرتا ہے جو رجسٹری فراہم کرتی ہے، اور ایک file: ڈیپینڈنسی آپ کی کاپی سے انسٹال ہوتی ہے:

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"
  }
}

ٹار بال اب آپ کی ریپوزٹری میں موجود ہے اور آپ کی مرضی کے بغیر تبدیل نہیں ہو سکتا۔ آپ نے ہمیشہ کے لیے اس کی اپ ڈیٹس کی ذمہ داری بھی لے لی ہے، لہذا اسے صرف ان چھوٹے متروک پیکیجز کے لیے استعمال کریں جن کے ساتھ آپ پھنس چکے ہیں، نہ کہ اپنے ویب فریم ورک کے لیے۔

ایک کولنگ آف پیریڈ (cooling-off period) بھی ہوتا ہے، جس کی کوئی قیمت نہیں:

npm install --before=2026-08-01

before آپشن ٹری کو صرف ان ورژنز کا استعمال کرتے ہوئے دوبارہ بناتا ہے جو اس تاریخ یا اس سے پہلے شائع ہوئے تھے۔ جب آپ ڈیپینڈنسیز کو ریفریش کریں تو اسے ایک یا دو ہفتے پیچھے سیٹ کر دیں، اس طرح آپ اس ونڈو سے بچ جائیں گے جس میں کوئی خراب ریلیز لائیو تو ہوتی ہے لیکن ابھی تک رپورٹ نہیں ہوئی ہوتی۔ یہ ایک غیر محتاط ٹول ہے، کیونکہ یہ حقیقی سیکیورٹی فکسز کو بھی روک دیتا ہے۔ اسے رینجز (ranges) کو حل کرنے کے لیے استعمال کریں، دیکھیں کہ کیا تبدیل ہوا ہے، اور پھر لاک فائل کو کمٹ کر دیں۔

مجھے کیسے معلوم ہوگا کہ میں نے اصل میں کون سا ورژن ریلیز کیا ہے؟

Git میں موجود lockfile یہ بتاتی ہے کہ کیا انسٹال ہونا چاہیے تھا۔ ڈسک یہ بتاتی ہے کہ کیا انسٹال ہوا ہے۔ صرف مؤخر الذکر ہی ثبوت ہے۔

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

npm ls، node_modules کو پڑھتا ہے، لہذا یہ اس چیز کی اطلاع دیتا ہے جو جسمانی طور پر موجود ہے، نہ کہ اس کی جو lockfile کا ارادہ تھا۔ node -e لائن انسٹال شدہ مینی فیسٹ کو پاتھ کے ذریعے پڑھتی ہے، جو ان پیکجز کے لیے بھی کام کرتی ہے جن کا exports فیلڈ سب پاتھ امپورٹس کو بلاک کرتا ہے، اور یہ بغیر کسی ٹری ڈرائنگ کے ایک ورژن پرنٹ کرتی ہے۔

موازنہ کے دوسرے حصے کے لیے، git کو پڑھیں:

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

کمٹ کو deploy لے آؤٹ میں ڈال کر دونوں کے درمیان مستقل تعلق قائم کریں۔ /srv/nodeapp/releases/<short commit sha> میں ریلیز کریں اور /srv/nodeapp/current کو symlink کے ذریعے اس کی طرف اشارہ کریں۔ "ابھی کیا چل رہا ہے" کا جواب readlink /srv/nodeapp/current بن جاتا ہے، اور یہ 03:00 بجے اس شخص کے لیے بھی دستیاب ہوتا ہے جس نے اسے ڈیپلائے نہیں کیا تھا۔

آخر میں، چیک کریں کہ رجسٹری کس چیز کی تصدیق کرتی ہے:

npm audit signatures

یہ آپ کے انسٹال شدہ ٹری میں موجود پیکجز پر رجسٹری کے دستخطوں کی تصدیق کرتا ہے، اور ان پیکجز کے لیے provenance attestations کی تصدیق کرتا ہے جن کے پاس وہ موجود ہیں۔ Provenance ایک شائع شدہ tarball کو اس پبلک continuous integration (CI) بلڈ سے جوڑتا ہے جس نے اسے تیار کیا، لہذا ایک تصدیق شدہ attestation کا مطلب ہے کہ آپ کوڈ کو کسی نامعلوم لیپ ٹاپ کے بجائے ایک کمٹ تک ٹریس کر سکتے ہیں۔ کوریج ہر جگہ نہیں ہے، لہذا کسی غائب attestation کو "کوئی معلومات نہیں" کے طور پر پڑھیں، نہ کہ "خراب پیکج" کے طور پر۔

جب کوئی خراب ریلیز آپ کے سرور تک پہنچ جائے تو کیا کریں

جو کچھ چلا اور جس صارف (user) کے تحت چلا، وہاں سے باہر کی طرف کام شروع کریں۔

اگر کوڈ انسٹالیشن کے دوران چلا تھا، تو فرض کر لیں کہ build user کی پہنچ میں موجود ہر چیز خطرے میں ہے۔ registry token، اس home directory میں موجود SSH keys، cloud credentials، اور وہ تمام secrets جو اس shell میں export کیے گئے تھے، انہیں تبدیل (rotate) کریں۔ rotation ہی واحد ایماندارانہ ردعمل ہے، کیونکہ آپ یہ ثابت نہیں کر سکتے کہ کوئی فائل پڑھی نہیں گئی ہے۔

اگر کوڈ runtime پر کسی locked-down service account کے تحت چلا تھا، تو پہنچ کا دائرہ کار بہت چھوٹا ہوتا ہے: صرف ایپلیکیشن کے اپنے environment variables اور وہ سب کچھ جہاں تک اس کی network رسائی ممکن ہے۔ unprivileged users کے طور پر VPS پر سروسز چلانے کا پورا استدلال یہی ہے۔ یہ compromise کو روکتا نہیں ہے۔ یہ طے کرتا ہے کہ مشین کا کتنا حصہ compromise ہوا ہے، اور کیا یہ restart کے بعد بھی برقرار رہتا ہے۔

پھر صفائی کرنے کے بجائے دوبارہ تعمیر (rebuild) کریں۔ node_modules کو حذف کریں، متاثرہ پیکیج کو package.json میں خراب ورژن سے نیچے پن (pin) کریں، lockfile کو اپ ڈیٹ کرنے کے لیے ایک بار npm install چلائیں، اسے commit کریں، اور npm ci کے ساتھ deploy کریں۔ کسی tree کو اپنی جگہ پر مرمت نہ کریں۔ آپ یہ شمار نہیں کر سکتے کہ انسٹال اسکرپٹ نے کن چیزوں کو چھیڑا ہے۔

اس وقت کے دورانیے (window) کو بھی لکھ لیں: پہلی deploy جس نے ممکنہ طور پر وہ ورژن حاصل کیا ہو، اور وہ deploy جس نے اسے ہٹایا۔ یہ رینج آپ کو بتاتی ہے کہ آپ کو اپنے کون سے logs پڑھنے ہیں، اور اس کا جواب صرف تبھی ممکن ہے اگر آپ کی ریلیزز کے نام commits کے نام پر رکھے گئے ہوں۔

جن مسائل کا حل ان میں سے کوئی بھی نہیں ہے

Lockfile کسی dependency کو محفوظ نہیں بناتا۔ یہ اس لمحے کو، جب آپ نے اس dependency کو قبول کیا تھا، ایک deploy کے ضمنی اثر (side effect) کے بجائے ایک پرانے، نظرثانی شدہ فیصلے میں تبدیل کر دیتا ہے۔ اوپر بیان کردہ ہر عمل اسی تبدیلی کو انجام دیتا ہے، یعنی حادثے کو انتخاب میں بدلنا۔

npm audit یہاں کوئی دفاع نہیں ہے۔ یہ آپ کے dependency tree کا موازنہ رپورٹ شدہ کمزوریوں (vulnerabilities) کے ڈیٹا بیس سے کرتا ہے، لہذا یہ صرف ان مسائل کو تلاش کرتا ہے جو پہلے سے شائع اور نامزد ہو چکے ہیں۔ supply-chain حملہ اپنی پوری مفید زندگی کے دوران نامعلوم رہتا ہے۔ پرانے معلوم بگز کے لیے npm audit چلائیں، لیکن چار گھنٹے پہلے ریلیز ہونے والی کسی چیز کے بارے میں اس سے کسی مدد کی توقع نہ رکھیں۔

اپنی dependencies کی تعداد کو کم کرنا اس گائیڈ میں موجود کسی بھی ٹول سے زیادہ مددگار ثابت ہوتا ہے، اور یہ سب سے کم مقبول مشورہ ہے۔ ہر وہ پیکیج جسے آپ شامل نہیں کرتے، اس کا مطلب ہے کہ ایک ایسا پبلشر کم ہو گیا جو آپ کی جانب سے فشنگ (phishing) کا شکار ہو سکتا ہے، اور ایک ایسی انسٹال اسکرپٹ کم ہو گئی جو آپ کے deploy user کے طور پر کبھی نہیں چلے گی۔

ان میں سے کوئی بھی بات صرف npm تک محدود نہیں ہے۔ یہی چار اصول PyPI، RubyGems، container images اور آپ کے ڈسٹری بیوشن کے پیکیج مینیجر پر بھی لاگو ہوتے ہیں۔ npm میں یہ سب سے زیادہ نمایاں ہے کیونکہ وہاں trees سب سے گہری ہوتی ہیں اور انسٹال اسکرپٹس بائی ڈیفالٹ چلتی ہیں۔ آپ کے ارد گرد کی مشین کا کتنا حصہ آپ کے دفاع کے لیے ہے، اس کا انحصار اس بات پر ہے کہ وہ کہاں چل رہی ہے، جو کہ اس وسیع تر سوال کا حصہ ہے کہ آیا VPS ہوسٹنگ محفوظ ہے۔

FAQ

کیا npm ci مجھے کسی compromised npm package سے محفوظ رکھتا ہے؟

یہ آپ کو ورژن کے آپ کی معلومات کے بغیر تبدیل ہونے سے محفوظ رکھتا ہے۔ npm ci بالکل وہی کچھ انسٹال کرتا ہے جو package-lock.json ریکارڈ کرتا ہے، ہر tarball کو اس کے sha512 integrity hash کے ساتھ چیک کرتا ہے، اور اگر package.json اور lockfile میں اختلاف ہو تو فرق کو حل کرنے کے بجائے error کے ساتھ exit کر جاتا ہے۔ یہ اس بارے میں کچھ نہیں کہتا کہ آیا pinned ورژن محفوظ ہے یا نہیں۔ اگر آپ ایسی lockfile commit کرتے ہیں جو کسی malicious ورژن کو پن (pin) کرتی ہے، تو npm ci ہر بار، آپ کے ہر سرور پر وفاداری سے وہی ورژن انسٹال کرے گا۔

کیا مجھے ہر چیز کے لیے ignore-scripts=true سیٹ کرنا چاہیے؟

اسے سیٹ کریں، پھر allowlist بنائیں۔ پروجیکٹ کی .npmrc میں ignore-scripts=true ڈیپینڈنسی انسٹال اسکرپٹس کو چلنے سے روکتا ہے، جو ایک خراب پیکیج سے آپ کے deploy user کے credentials تک پہنچنے کا سب سے براہ راست راستہ ختم کر دیتا ہے۔ جو پیکیجز native addon کمپائل کرتے ہیں یا prebuilt binary لاتے ہیں، انہیں واقعی اس کی ضرورت ہوتی ہے، اور اسکرپٹس بند ہونے پر وہ انسٹالیشن کے وقت کے بجائے بعد میں runtime پر missing binding file کے ساتھ فیل ہو جاتے ہیں۔ npm ci --ignore-scripts چلائیں، پھر ان چند پیکیجز کے لیے npm rebuild <package> چلائیں جن پر آپ نے بھروسہ کرنے کا فیصلہ کیا ہے۔ npm query ":attr(scripts, [postinstall])" دکھاتا ہے کہ اصل میں ایسے کتنے پیکیجز ہیں۔

میں کیسے معلوم کروں کہ میرے سرور نے حقیقت میں پیکیج کا کون سا ورژن انسٹال کیا ہے؟

ڈسک کو پڑھیں، lockfile کو نہیں۔ npm ls <package> رپورٹ کرتا ہے کہ node_modules میں کیا موجود ہے، اور node -e "console.log(require('./node_modules/<package>/package.json').version)" صرف ورژن سٹرنگ پرنٹ کرتا ہے۔ git میں موجود lockfile ایک مختلف سوال کا جواب دیتی ہے، یعنی کیا انسٹال ہونا چاہیے تھا، اور ان دونوں کا موازنہ کرنا ہی اصل مقصد ہے۔ git commit کے نام سے منسوب ڈائریکٹری میں deploy کرنے سے مہینوں بعد بھی دونوں جوابات دستیاب رہتے ہیں، جب آپ کو ان کی ضرورت ہو۔

کیا npm audit سپلائی چین حملوں (supply-chain attacks) کو تلاش کرتا ہے؟

نہیں۔ npm audit آپ کی ٹری (tree) کا موازنہ رپورٹ شدہ کمزوریوں (vulnerabilities) کے ڈیٹا بیس سے کرتا ہے، لہذا یہ صرف وہی مسائل تلاش کرتا ہے جو پہلے سے شائع ہو چکے ہوں اور جنہیں شناخت کنندہ (identifier) دیا گیا ہو۔ ایک malicious ریلیز ان گھنٹوں یا دنوں کے دوران غیر رپورٹ شدہ رہتی ہے جب اس کا انسٹال ہونا اہم ہوتا ہے۔ npm audit signatures زیادہ مفید کمانڈ ہے: یہ آپ کی انسٹال شدہ ٹری میں registry signatures کی تصدیق کرتی ہے اور provenance attestations کو چیک کرتی ہے جہاں پبلشر نے انہیں تیار کیا ہو، جو آپ کو بتاتا ہے کہ tarball کسی نامعلوم مشین کے بجائے پبلک بلڈ سے آیا ہے۔

اگر حملہ انسٹالیشن کے وقت ہوتا ہے تو unprivileged user کے طور پر ایپ چلانا کیوں اہم ہے؟

کیونکہ دونوں ناکامیوں کی پہنچ مختلف ہوتی ہے اور آپ دونوں کے خلاف دفاع کر رہے ہوتے ہیں۔ انسٹالیشن کے وقت چلنے والا کوڈ deploy user کے طور پر چلتا ہے اور اس صارف کی SSH keys، registry tokens اور cloud credentials پڑھ سکتا ہے۔ Runtime کوڈ service account کے طور پر چلتا ہے، اور User=nodeapp، ProtectSystem=strict اور ڈسک پر کسی بھی قابلِ رسائی credentials کے بغیر، اس کی پہنچ صرف ایپلیکیشن کے اپنے ماحول اور اس کے ڈیٹا بیس تک محدود رہتی ہے۔ اکاؤنٹس کو الگ کرنے کا مطلب یہ بھی ہے کہ ٹریفک سنبھالنے والا پروسیس node_modules کو دوبارہ نہیں لکھ سکتا، لہذا runtime پر ہونے والا سمجھوتہ (compromise) مستقل ہونے کے بجائے اگلے ری اسٹارٹ پر ختم ہو جاتا ہے۔