npm Tedarik Zinciri Saldırısı Nasıl Önlenir?
npm paketleri üzerinden sunucunuza sızan kötü niyetli kodları engelleyin. postinstall betikleri, typosquatting ve güvensiz dağıtım süreçlerine karşı teknik önlemleri öğrenin.
npm tedarik zinciri saldırısı nedir
Bir npm tedarik zinciri saldırısı, sunucunuza yüklemeyi seçtiğiniz bir paket aracılığıyla ulaşır. Bu süreçte açık bir port veya bir istismar adımı yoktur. npm (node package manager) kod yükler ve kod yüklemek, kodun çalıştırılması anlamına gelir. Bu nedenle küçük bir Node uygulaması, daha önce hiç incelemediğiniz yüzlerce paketi içeri çeker ve bunlardan herhangi biri bir saat sonra yeni bir sürüm yayınlayabilir.
Dağıtımınız kötü niyetli bir sürümü çeker çünkü yükleme komutunuz eşleşen en yeni sürümü talep etmiştir. Bu kod, yükleme işlemini kim çalıştırdıysa onun yetkileriyle çalışır. Aşağıdaki her şey bu iki cümleden kaynaklanır.
Şekiller, tek bir Node uygulamasını tek bir VPS'e dağıtan bir kişiyi ne sıklıkla etkilediklerine göre sıralanmıştır. Bu sıralama, büyük bir şirketin kullanacağı sıralama değildir; çünkü büyük bir şirketin dahili bir kayıt defteri, bir inceleme ekibi ve genel kayıt defterinin bir yansıması bulunur. Sizin ise bir dağıtım betiğiniz vardır.
Senaryo 1: Bir sürdürücü hesabı ele geçirilir ve yamalı bir sürüm yayınlanır
npm kayıt defteri, halihazırda var olan bir sürümün içeriğinin değiştirilmesine izin vermez. Bir sürdürücüyü oltalama yöntemiyle kandıran veya yayınlama anahtarını çalan bir saldırgan, bu nedenle 4.18.2 içeriğini yeniden yazamaz. Bunun yerine 4.18.3 yayınlarlar.
package.json dosyanızı inceleyin. "express": "^4.18.2" gibi bir satır, 4.18.2 sürümü anlamına gelmez. Şapka işareti "bu sürüm veya üzerindeki herhangi bir 4.x sürümü", ~4.18.2 ise "herhangi bir 4.18.x sürümü" anlamına gelir. npm install, bu aralığı çalıştırıldığı anda çözümler; dolayısıyla aynı gün içinde iki kez dağıtılan aynı git commit'i, iki farklı kod kümesi yükleyebilir. Bu boşluk, saldırı yüzeyini oluşturur. Bu durumun gerçekleşmesi için makinenizdeki hiçbir şeyin ele geçirilmiş olması gerekmez.
Kötü niyetli sürümler genellikle rapor edilir ve kaldırılır, ancak kaldırma işlemi insanlar bunları yükledikten sonra gerçekleşir. O pencere aralığında dağıtım yapan herkes, ilgili kodu diskinde bulundurur. Her çalıştırmada aralıkları çözümleyen bir pipeline, kimse karar vermese bile haftada birkaç kez otomatik olarak bu pencereye girer.
Biçim 2: Bir kurulum betiğinin dağıtımı yapan kullanıcı yetkileriyle çalışması
Bir paketin package.json dosyası, scripts bloğu içerisinde preinstall, install, postinstall ve prepare tanımlayabilir. npm bu betikleri kurulum sırasında çalıştırır. Bu betikler korumalı bir alanda (sandbox) çalışmaz ve kimse tarafından denetlenmez. Bunlar, kurulum komutunu yazan kullanıcının yetkileriyle, o kullanıcının ev dizininde, kullanıcının ağ erişimiyle ve o kabuğun tüm ortam değişkenleriyle çalışan kabuk komutlarıdır.
Bu durumda sorulması gereken anlamlı soru, paketin ne yapabileceği değil, kullanıcının neleri okuyabileceğidir. Normal bir dağıtım sunucusunda bu cevap; kayıt defteri (registry) anahtarını tutan ~/.npmrc, SSH (secure shell) için dağıtım anahtarı olarak kullanılan ~/.ssh/id_ed25519, ~/.aws/credentials, ~/.docker/config.json ve genellikle DATABASE_URL değerinin bulunduğu kabuktaki tüm dışa aktarılmış değişkenleri kapsar.
Bunun gibi bir yük (payload), kalıcılığa veya ayrıcalık yükseltmeye ihtiyaç duymaz. Sadece birkaç dosyayı okur, bunları HTTPS üzerinden bir sunucuya gönderir ve 0 durum koduyla çıkar. npm, kurulum betiği çıktılarını varsayılan olarak gizlediği için hiçbir şey görmezsiniz. Bunu devre dışı bırakın ve gerçekte nelerin çalıştığını izleyin:
npm ci --foreground-scriptsforeground-scripts, standart girdiyi, çıktıyı ve hatayı npm süreciyle paylaşır; böylece derleme betikleri, npm'in kurulum başarılı olduğunda attığı bir arabelleğe değil, doğrudan terminalinize çıktı verir.
Biçim 3: typosquat ve tam olarak yazmadığınız isim
Typosquat, popüler bir paketin ismine çok benzeyen bir isimle yayınlanan ve hatalı yazılmış ya da yanlış yapıştırılmış bir kurulum komutunu bekleyen pakettir. Mekanizma kod değil komut üzerine kuruludur, bu nedenle lockfile burada size yardımcı olmaz: yanlış ismi bir kez eklersiniz ve o andan itibaren lockfile bunu sadakatle sabitler.
Bireylerden ziyade ekipleri hedef alan varyant ise bağımlılık karmaşasıdır (dependency confusion). Dahili paketinizin adı billing-utils olsun ve özel bir kayıt defterinde (registry) barınsın. Eğer halka açık kayıt defterinde billing-utils adında hiçbir şey yoksa, herhangi biri bu isimle bir paket yayınlayabilir. npm, kapsamı belirtilmemiş (unscoped) isimleri varsayılan halka açık kayıt defterinde çözümler, bu yüzden halka açık kopya kazanabilir. Bunun çözümü, sahip olduğunuz bir kapsam (scope) ve bu kapsam için .npmrc dosyasında tanımlanan bir kayıt defteri eşleştirmesidir:
@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}Artık @yourorg/billing-utils yalnızca o ana bilgisayardan getirilir, çünkü kapsamdan kayıt defterine eşleme, varsayılan kayıt defterinden önce kontrol edilir. Kapsamı belirtilmemiş dahili bir ismin eşlemesi yoktur, bu nedenle hiçbir koruması da yoktur.
Yeni bir bağımlılık eklemeden önce, indirme rozetine değil, paketin kendisine bakın:
npm view some-lib repository.url maintainers time.created time.modifiedGeçen ay oluşturulmuş ve halka açık bir depoyla ilişkilendiremediğiniz bir hesap tarafından yayınlanmış bir paket, altı yıllık geçmişi olan bir paketten farklı bir risk taşır. Her iki durum da tek başına kanıt değildir. Ancak her ikisini de kontrol etmenin maliyeti düşüktür.
Şekil 4: sahibi sessizce değişen bağımlılık
Bakımcılar paketleri devreder. Birisi yorulur, bir yabancı yardım teklif eder, yayınlama hakları el değiştirir ve buna bağımlı olan projelere hiçbir bildirim ulaşmaz. Hiçbir şey tehlikeye girmez. 2021 yılında verdiğiniz güven artık farklı bir kişinin elindedir.
Bu, en yavaş ilerleyen ve tespit edilmesi en zor şekildir; hiçbir komut buna doğrudan yanıt vermez. İki yöntem bu durumu daraltır. Bir paketi benimsemeden önce, yukarıdaki npm view satırı ile kimin yayın yapabileceğini kontrol edin. Ardından, gerçekten bağımlı olduğunuz bir paket taşındığında farkları (diff) okuyun:
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İlk biçim yalnızca değişen dosya adlarını yazdırır; bu, önemsediğiniz bir paketin her yükseltmesinde yapılabilecek kadar hızlıdır. Bir derleme betiğine dokunan, paket köküne yeni bir dosya ekleyen veya scripts bloğunu düzenleyen bir yama sürümü, sunucunuza ulaşmadan önce bütünüyle okunmaya değerdir.
npm ci ile kilitli dosya üzerinden derleme
package-lock.json, ağaçtaki her paketin tam sürümünü, geldiği URL adresini, her bir tarball dosyasının sha512 bütünlük özetini ve hangi paketin buna ihtiyaç duyduğunu kaydeder. Bu dosyayı commit edin. Gerçekte neyi test ettiğinizi belirten tek dosya budur.
Ardından, geliştirici dizüstü bilgisayarı olmayan herhangi bir makinede npm install yerine mutlaka npm ci kullanarak kurulum yapın:
npm ci --omit=dev --ignore-scriptsnpm ci, npm install komutundan burada önem arz eden şekillerde ayrılır. Bir kilitli dosyanın (lockfile) mevcut olmasını gerektirir. Başlamadan önce mevcut node_modules dizinini siler; böylece önceki bir dağıtımdan kalan artıklar bu dağıtıma taşınmaz. package.json dosyasına veya kilitli dosyaya hiçbir zaman yazma yapmaz; bu sayede bir kurulum işlemi sizi sessizce bir üst sürüme taşıyamaz. Kilitli dosya ile package.json arasında uyumsuzluk varsa, farkı çözmeye çalışmak yerine bir hata vererek sonlanır.
Bu hata bir rahatsızlık değil, bir özelliktir. Bağımlılık değişikliğinin saat 02:00'deki bir dağıtımın yan etkisi olarak değil, birinin incelediği bir commit olarak gelmesi gerektiğini ifade eder.
Bütünlük özeti (integrity hash) her indirme işleminde kontrol edilir. Baytları kayıtlı özetle eşleşmeyen bir tarball, paketi açmak yerine code EINTEGRITY hatası vererek kurulumu başarısız kılar. Bunun size ne kazandırdığı konusunda net olun: Aldığınız dosyanın, kilitli dosyanın sabitlediği dosya olduğunu kanıtlar; bu, indirmeleri sağlama toplamı ile doğrulama işleminin size sunduğu garantinin aynısıdır ve aynı sınırlara sahiptir. Sabitlenen sürümün yayınlandığı sırada kötü niyetli olup olmadığı hakkında hiçbir şey söylemez.
--omit=dev hakkında bir detay: bu paketler yine çözümlenir ve yine kilitli dosyaya yazılır. Sadece diske yerleştirilmezler. Diskte daha az paket olması, daha az kurulum betiği ve çalışma zamanında daha az kod yüklenmesi anlamına gelir; bu nedenle yapmaya değerdir. Bu işlem, bağımlılığı ağacınızdan kaldırmaz.
Kurulum betiklerini kod olarak değerlendirin ve bunları nasıl reddedeceğinizi öğrenin
Kurulum betiklerini devre dışı bırakabilirsiniz. Bunun için projenin .npmrc dosyasına aşağıdaki satırı ekleyin ve bunu lockfile ile birlikte commit edin:
ignore-scripts=true
save-exact=trueignore-scripts=true, npm'in bağımlılıklarda tanımlanan betikleri çalıştırmasını engeller. save-exact=true, npm install some-lib aracının 1.4.2 değerini ^1.4.2 yerine package.json içine yazmasını sağlar; böylece çözümlenen bir aralık yanlışlıkla manifest dosyanıza girmez.
Bu işlem bazı yapıları bozabilir; bu nedenle etkinleştirmeden önce nasıl çalıştığını bilmeniz gerekir. Yerel bir eklenti (native addon) derleyen veya önceden derlenmiş bir ikili dosyayı indiren paketler, bu işlemi bir kurulum betiği içinde gerçekleştirir. Betikler kapalıyken kurulumun kendisi başarılı olur ancak hata daha sonra, çalışma zamanında, binding dosyasını yükleyemeyen bir modül olarak ortaya çıkar. Bunun çözümü bir izin listesi (allowlist) oluşturmaktır:
npm ci --ignore-scripts
npm rebuild better-sqlite3npm rebuild <package>, yalnızca ilgili paket için derleme betiklerini çalıştırır. Böylece, hiç tanımadığınız yüzlerce yabancıya genel bir yürütme izni vermek yerine, paket bazında bilinçli bir karar vermiş olursunuz.
Bu iznin şu an ne kadar geniş olduğunu görmek için npm'e şu komutu sorun:
npm query ":attr(scripts, [postinstall])"Bu komut, yüklü ağaçta postinstall betiği barındıran tüm paketleri listeler. Tipik bir uygulamada bu liste beklenenden daha kısadır; izin listesini pratik kılan da tam olarak budur.
Derleme süreci ile trafiği karşılayan süreci birbirinden ayırın
Deploy kullanıcısının node_modules dizinine yazma yetkisine sahip olması gerekir. HTTP isteklerini yanıtlayan sürecin ise buna ihtiyacı yoktur. Eğer her iki işlem aynı hesap üzerinden yürütülürse, kurulum sırasında çalışan kod, kullanıcılara hizmet veren kodu yeniden yazabilir; aynı şekilde çalışma zamanında çalışan kod da bunu yapabilir.
Bu süreçleri ayırın. Derleme işlemini bir kullanıcı ile, servis etme işlemini başka bir kullanıcı ile gerçekleştirin ve servis edilen dizini, ilgili hesap için salt okunur hale getirin:
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/nodeappArdından bunu systemd ile zorunlu kılın. /etc/systemd/system/nodeapp.service dosyasını oluşturun:
[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.targetProtectSystem=strict direktifi, /dev, /proc, /sys ve ReadWritePaths içerisinde listelediğiniz dizinler haricinde, tüm dosya sistemini bu servis için salt okunur hale getirir. Bu nedenle uygulamanın node_modules dizinine yazma girişimi EROFS: read-only file system hatasıyla başarısız olur; bu durumu kendi loglarınızda bir dakika içerisinde doğrulayabilirsiniz. NoExecPaths seçeneği, yazılabilir yükleme dizinini kapsar: servis buraya dosya yazabilir ancak çekirdek bu dosyaların çalıştırılmasına izin vermez. Bu seçenek systemd 249 veya daha yeni bir sürüm gerektirir; Ubuntu 24.04 ise 255 sürümüyle gelmektedir.
Bu unit dosyasında iki tuzak bulunmaktadır. Birincisi, MemoryDenyWriteExecute=yes seçeneğini eklemeyin. Çoğu systemd sıkılaştırma listesinde yer alsa da, Node'un çalışmasını engeller; çünkü V8, JavaScript'i çalışma zamanında makine koduna derler ve hem yazılabilir hem de çalıştırılabilir bellek sayfalarına ihtiyaç duyar. İkincisi, command -v node içerisindeki ExecStart yolunu dikkate alın. Eğer Node bir sürüm yöneticisi ile kurulduysa, deploy kullanıcısının ev dizini altında yer alır; bu durumda ProtectHome=yes o dizini servisten gizler ve unit dosyası, yürütülebilir dosyanın bulunamadığını belirten status=203/EXEC hatasıyla derhal başarısız olur.
Dosyaya güvenmek yerine sonucu kontrol edin:
sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemd-analyze security nodeapp.service
sudo -u nodeapp touch /srv/nodeapp/current/probesystemd-analyze security komutu, her bir sıkılaştırma ayarını ve maruziyet durumunu listeler; böylece hangilerinin varsayılan değerde kaldığını görebilirsiniz. touch komutu Permission denied hatasıyla başarısız olmalıdır, çünkü nodeapp kullanıcısı current altındaki hiçbir dosyanın sahibi değildir. Eğer komut başarılı olursa, dosya sahiplikleriniz hatalıdır ve systemd ayarları bu durumu sessizce maskeliyordur.
EnvironmentFile hakkında bir not: systemd, kullanıcıyı User=nodeapp olarak değiştirmeden önce bu dosyayı root yetkileriyle okur; bu nedenle dosya 600 izinleriyle root:root olarak ayarlanabilir. Uygulama değişkenleri yine de alır. nodeapp kullanıcısı olarak shell erişimi olan herkes bu değişkenleri /proc/<pid>/environ üzerinden okuyabilir; dolayısıyla bu yöntem sırrı çalışma zamanında değil, diskte durduğu sürece korur.
Dağıtım kimlik bilgilerini derleme ortamının dışında tutun
Kurulum betikleri ortam değişkenlerini devralır. Bu tek gerçek, derleme işlemini nerede yapacağınıza karar vermenizi sağlamalıdır.
En güvenli yöntem, derlemeyi üretim sunucusu olmayan bir yerde yapıp bitmiş dizini sunucuya kopyalamaktır. Derleme makinesi yalnızca salt okunur bir kayıt defteri anahtarı tutar, başka hiçbir şey bulundurmaz. SSH dağıtım anahtarı, bulut erişim anahtarı, veritabanı parolası veya container kayıt defteri girişi içermez.
npm token create --read-onlySalt okunur bir anahtar paketleri çekebilir ancak yayınlama yapamaz. Bir derleme ortamından çalınması durumunda oluşacak kayıp, yalnızca genel paketleri indirme yeteneği ile sınırlıdır.
Eğer derlemeyi mutlaka sunucu üzerinde yapmanız gerekiyorsa, işlemi kısıtlı bir ortama sahip deploy kullanıcısı ile gerçekleştirin ve çalışma zamanı sırlarını deploy kullanıcısının okuyamayacağı /etc/nodeapp/env dizininde tutun. Aynı mantık, kendi barındırdığınız derleme otomasyonları için de geçerlidir: kendi barındırdığınız bir GitHub Actions runner, anahtarları tutar ve her iş sırasında rastgele yayınlanmış kodları çalıştırır; bu da onu küçük bir dağıtımdaki en değerli makine haline getirir. Yazmadığınız ve tüm ortamınıza erişimi olan her program aynı kategoridedir; bu nedenle sırları bir yapay zeka aracısının ortamından uzak tutmak, aynı sorunun farklı bir program üzerinden ele alınmasıdır.
Denetleyemediğiniz paketleri sabitleyin veya vendor dizinine alın
Sabitlenmiş bir bağımlılık, sürümü bir commit işlemi olmadan değişemeyen bağımlılıktır. Commit edilmiş bir lockfile, tüm ağaç için zaten bunu sağlar. Ancak iki durum daha fazlasını gerektirir.
İlki geçişli (transitive) bağımlılıklardır. Bağımlılıklarınızın neye bağımlı olduğunu kontrol edemezsiniz. overrides içindeki package.json, ağacın herhangi bir yerindeki sürümü zorunlu kılar:
{
"overrides": {
"some-transitive-lib": "1.4.2"
}
}Bunu ekledikten sonra lockfile dosyasının sonucu kaydetmesi için npm install komutunu bir kez çalıştırın ve ardından her iki dosyayı da commit edin.
İkinci durum ise denetleyemediğiniz ve vazgeçemediğiniz bir pakettir. Bu paketi vendor dizinine alın. npm pack, kayıt defterinin sunacağı tarball dosyasının aynısını indirir ve bir file: bağımlılığı, kurulumu sizin kopyanız üzerinden yapar:
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 dosyası artık deponuzda yaşar ve sizden habersiz değişemez. Ayrıca güncellemelerinin sorumluluğunu da kalıcı olarak üstlenmiş olursunuz; bu nedenle bu yöntemi web framework'ünüz için değil, bağımlı kaldığınız küçük ve terk edilmiş paketler için kullanın.
Ayrıca hiçbir maliyeti olmayan bir bekleme süresi (cooling-off period) de mevcuttur:
npm install --before=2026-08-01before seçeneği, ağacı yalnızca o tarihte veya öncesinde yayınlanmış sürümleri kullanarak yeniden oluşturur. Bağımlılıkları yenilerken bu tarihi bir veya iki hafta geriye ayarlayın; böylece hatalı bir sürümün yayında olduğu ancak henüz bildirilmediği zaman aralığını atlamış olursunuz. Bu kaba bir araçtır çünkü gerçek güvenlik yamalarını da engeller. Aralıkları çözümlemek, nelerin değiştiğini okumak ve ardından lockfile dosyasını commit etmek için kullanın.
Hangi sürümü yayınladığımı nasıl anlarım?
Git içindeki lockfile, neyin kurulmuş olması gerektiğini belirtir. Disk ise neyin kurulu olduğunu gösterir. Yalnızca ikincisi kanıt niteliğindedir.
npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"npm ls, node_modules dosyasını okur; bu nedenle lockfile'ın amaçladığı sürümü değil, fiziksel olarak mevcut olanı raporlar. node -e satırı, kurulu manifesti yol üzerinden okur. Bu yöntem, exports alanı alt yol içe aktarımlarını engelleyen paketler için bile çalışır ve herhangi bir ağaç çizimi olmaksızın tek bir sürüm yazdırır.
Karşılaştırmanın diğer yarısı için git kayıtlarını okuyun:
git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'Commit bilgisini deploy düzenine ekleyerek ikisi arasındaki bağlantıyı kalıcı hale getirin. /srv/nodeapp/releases/<short commit sha> içine yayınlayın ve bir symlink ile /srv/nodeapp/current işaretçisini buraya yönlendirin. "Şu anda ne çalışıyor" sorusunun cevabı readlink /srv/nodeapp/current olur ve bu bilgi, dağıtımı yapan kişi dışında biri tarafından saat 03:00'te bile erişilebilir durumdadır.
Son olarak, registry'nin neyi onayladığını kontrol edin:
npm audit signaturesBu işlem, kurulu ağacınızdaki paketlerin registry imzalarını doğrular ve varsa paketlerin köken (provenance) beyanlarını teyit eder. Köken beyanı, yayınlanmış bir tarball dosyasını onu üreten herkese açık continuous integration (CI) derlemesine bağlar. Doğrulanmış bir beyan, kodun kaynağını bilinmeyen bir dizüstü bilgisayara değil, bir commit kaydına kadar takip edebileceğiniz anlamına gelir. Kapsam evrensel değildir; bu nedenle eksik bir beyanı "kötü paket" olarak değil, "bilgi yok" şeklinde yorumlayın.
Hatalı bir sürüm sunucunuza ulaştığında yapılması gerekenler
Neyin ve hangi kullanıcı yetkisiyle çalıştığından yola çıkarak dışa doğru ilerleyin.
Kod kurulum sırasında çalıştıysa, derleme kullanıcısının okuyabildiği her şeyin ele geçirildiğini varsayın. Kayıt defteri belirtecini, ilgili ev dizinindeki SSH anahtarlarını, bulut kimlik bilgilerini ve o kabukta dışa aktarılan tüm gizli verileri döndürün (rotate). Bir dosyanın okunmadığını kanıtlayamayacağınız için tek dürüst yanıt döndürme işlemidir.
Kod, kısıtlanmış bir servis hesabı altında çalışma zamanında çalıştıysa, erişilebilir küme çok daha küçüktür: uygulamanın kendi ortam değişkenleri ve ağ erişiminin ulaşabildiği yerler. Servisleri bir VPS üzerinde ayrıcalıksız kullanıcılarla çalıştırmanın tüm mantığı budur. Bu, ele geçirilme durumunu engellemez. Sadece ele geçirme işleminin makinenin ne kadarına erişebileceğine ve yeniden başlatma sonrasında varlığını sürdürüp sürdüremeyeceğine karar verir.
Ardından temizlemek yerine yeniden oluşturun. node_modules dizinini silin, etkilenen paketi package.json içerisinde hatalı sürümün altında sabitleyin, kilit dosyasını güncellemek için npm install komutunu bir kez çalıştırın, commit edin ve npm ci ile dağıtımı gerçekleştirin. Bir dizin ağacını yerinde onarmaya çalışmayın. Bir kurulum betiğinin nerelere dokunduğunu tek tek listeleyemezsiniz.
Ayrıca zaman aralığını da not edin: sürümü çekmiş olabilecek ilk dağıtım ve onu kaldıran dağıtım. Bu aralık, hangi günlük kayıtlarınızı incelemeniz gerektiğini söyler ve bu ancak sürümleriniz commit isimleriyle adlandırılmışsa yanıtlanabilir.
Bunların çözemediği sorunlar
Bir lockfile, bir bağımlılığı güvenli hale getirmez. Sadece o bağımlılığı kabul ettiğiniz anı, bir dağıtımın yan etkisi olmaktan çıkarıp tarihli ve gözden geçirilmiş bir karara dönüştürür. Yukarıdaki her uygulama, aynı dönüşümü gerçekleştirerek kazaları birer tercihe çevirir.
npm audit burada bir savunma mekanizması değildir. Bağımlılık ağacınızı raporlanmış güvenlik açıkları içeren bir veritabanı ile karşılaştırır; dolayısıyla yalnızca önceden yayınlanmış ve adlandırılmış sorunları bulur. Bir tedarik zinciri saldırısı, faydalı ömrü boyunca isimsiz kalır. Bilinen eski hatalar için npm audit çalıştırın ancak dört saat önce yayınlanmış bir sürüm hakkında bu araçtan hiçbir şey beklemeyin.
Bağımlılık sayınızı azaltmak, bu kılavuzdaki herhangi bir araçtan daha fazla fayda sağlar ve bu, verilen en az popüler tavsiyedir. Eklemediğiniz her paket, sizin adınıza kimlik avına uğratılamayacak bir yayıncı ve deploy kullanıcınız olarak asla çalışmayacak bir kurulum betiği demektir.
Bunların hiçbiri sadece npm ile sınırlı değildir. Aynı dört temel durum PyPI, RubyGems, container imajları ve dağıtımınızın paket yöneticisi için de geçerlidir. npm'de daha belirgin olmasının nedeni, ağaçların çok derin olması ve kurulum betiklerinin varsayılan olarak çalıştırılmasıdır. Çevredeki makinenin ne kadarının savunulması gerektiği, nerede çalıştığına bağlıdır; bu da VPS barındırmanın güvenli olup olmadığına dair daha geniş sorunun bir parçasıdır.
FAQ
npm ci beni ele geçirilmiş bir npm paketinden korur mu?
Sizin bilginiz dışında sürümün değişmesini engeller. npm ci, package-lock.json dosyasının kaydettiği içeriği tam olarak kurar, her bir tarball dosyasını sha512 bütünlük hash değeri ile doğrular ve package.json ile kilit dosyası (lockfile) arasında uyuşmazlık olması durumunda farkı gidermeye çalışmak yerine hata vererek durur. Sabitlenen sürümün güvenli olup olmadığı konusunda bir değerlendirme yapmaz. Eğer kötü niyetli bir sürümü sabitleyen bir kilit dosyasını commit ederseniz, npm ci bu sürümü sahip olduğunuz her sunucuya her seferinde sadakatle kuracaktır.
ignore-scripts=true ayarını her şey için yapmalı mıyım?
Önce bu ayarı yapın, ardından izin verilenler listesi (allowlist) oluşturun. Projenin .npmrc dosyasındaki ignore-scripts=true ayarı, bağımlılık kurulum betiklerinin çalışmasını durdurur; bu da kötü niyetli bir paketin deploy kullanıcınızın kimlik bilgilerine ulaşması için kullandığı en doğrudan yolu kapatır. Yerel bir eklentiyi derleyen veya önceden oluşturulmuş bir binary dosyasını çeken paketlerin betiklere gerçekten ihtiyacı vardır; betikler kapalıyken bu paketler kurulum aşamasında değil, çalışma zamanında eksik binding dosyası hatası vererek başarısız olur. npm ci --ignore-scripts komutunu çalıştırın, ardından güvendiğiniz az sayıdaki paket için npm rebuild <package> komutunu kullanın. npm query ":attr(scripts, [postinstall])" komutu, gerçekte kaç tane paket olduğunu gösterir.
Sunucumun aslında hangi paket sürümünü kurduğunu nasıl öğrenebilirim?
Kilit dosyasına değil, diske bakın. npm ls <package>, node_modules dizininde nelerin mevcut olduğunu raporlar; node -e "console.log(require('./node_modules/<package>/package.json').version)" ise yalnızca sürüm dizisini yazdırır. Git içindeki kilit dosyası, "ne kurulmuş olması gerektiği" sorusuna yanıt verir; ikisini karşılaştırmak zaten asıl amaçtır. Uygulamayı git commit adını taşıyan bir dizine deploy etmek, her iki cevabı da aylar sonra ihtiyaç duyduğunuzda elinizin altında tutmanızı sağlar.
npm audit tedarik zinciri saldırılarını tespit eder mi?
Hayır. npm audit, ağaç yapınızı raporlanmış güvenlik açıkları veritabanı ile karşılaştırır; bu nedenle yalnızca önceden yayınlanmış ve bir tanımlayıcı atanmış sorunları bulur. Kötü niyetli bir sürüm, kurulumun kritik olduğu saatler veya günler boyunca raporlanmamış olabilir. npm audit signatures daha kullanışlı bir komuttur: kurulu ağacınızdaki kayıt defteri imzalarını doğrular ve yayıncının sağladığı durumlarda köken kanıtlarını (provenance attestations) kontrol eder. Bu, bir tarball dosyasının bilinmeyen bir makineden değil, genel bir yapı sisteminden geldiğini size bildirir.
Saldırı kurulum aşamasında gerçekleşiyorsa, uygulamayı yetkisiz bir kullanıcıyla çalıştırmak neden önemlidir?
Çünkü bu iki başarısızlık türünün etki alanı farklıdır ve her ikisine karşı da savunma yapmanız gerekir. Kurulum aşamasındaki kod, deploy kullanıcısı yetkileriyle çalışır ve bu kullanıcının SSH anahtarlarını, kayıt defteri token'larını ve bulut kimlik bilgilerini okuyabilir. Çalışma zamanındaki kod ise servis hesabı yetkileriyle çalışır; User=nodeapp, ProtectSystem=strict ve diskte okunabilir kimlik bilgileri bulunmadığında, saldırganın erişimi yalnızca uygulamanın kendi ortamı ve veritabanı ile sınırlı kalır. Hesapları ayırmak, trafiği karşılayan sürecin node_modules dosyasını yeniden yazamayacağı anlamına gelir; böylece çalışma zamanındaki bir ihlal, kalıcı hale gelmek yerine bir sonraki yeniden başlatmada temizlenmiş olur.