Serangan Supply Chain npm pada Server Node.js
Pelajari cara serangan supply chain npm mencapai aplikasi Node.js di VPS melalui patch berbahaya, skrip postinstall, typosquat, dan praktik deploy yang mencegahnya.
Apa yang dimaksud dengan serangan supply chain npm pada server Anda
Serangan supply chain npm mencapai server Anda melalui package yang Anda pilih untuk diinstal. Tidak ada port yang terbuka dan tidak ada tahap eksploitasi. npm (node package manager) menginstal kode, dan menginstal kode berarti menjalankan kode. Karena itu, aplikasi Node kecil dapat menarik beberapa ratus package yang belum pernah Anda baca, dan salah satunya dapat menerbitkan versi baru satu jam dari sekarang.
Proses deploy Anda mengambil versi berbahaya karena perintah instalasi meminta versi terbaru yang sesuai. Kode tersebut kemudian berjalan dengan hak akses pengguna yang menjalankan instalasi. Semua penjelasan di bawah ini berasal dari dua kalimat tersebut.
Bentuk serangan di bawah ini diurutkan berdasarkan seberapa sering serangan tersebut menargetkan satu orang yang melakukan deploy satu aplikasi Node ke satu VPS. Urutan ini bukan urutan yang akan digunakan perusahaan besar, karena perusahaan besar memiliki registry internal, tim peninjau, dan mirror dari registry publik. Anda memiliki skrip deploy.
Bentuk 1: akun maintainer dibobol lalu menerbitkan patch
npm registry tidak mengizinkan siapa pun mengubah isi versi yang sudah ada. Karena itu, penyerang yang memancing maintainer melalui phishing atau mencuri publish token tidak dapat menulis ulang 4.18.2. Mereka menerbitkan 4.18.3.
Periksa package.json Anda. Baris seperti "express": "^4.18.2" bukan berarti versi 4.18.2. Tanda caret berarti “versi 4.x apa pun yang sama atau lebih tinggi dari versi ini”, sedangkan ~4.18.2 berarti “versi 4.18.x apa pun”. npm install menyelesaikan rentang tersebut saat dijalankan. Akibatnya, commit git yang sama, yang di-deploy dua kali pada sore yang sama, dapat menginstal dua kumpulan kode yang berbeda. Celah itulah permukaan serangannya. Tidak ada apa pun di mesin Anda yang perlu dibobol agar celah tersebut terbuka.
Rilis berbahaya biasanya dilaporkan dan ditarik, tetapi penarikan itu terjadi setelah orang menginstalnya. Siapa pun yang melakukan deployment selama periode tersebut sudah memiliki kode itu di disk. Pipeline yang menyelesaikan rentang versi pada setiap proses berjalan otomatis dalam periode tersebut, beberapa kali seminggu, tanpa keputusan siapa pun.
Bentuk 2: skrip instalasi berjalan sebagai pengguna yang melakukan deploy
package.json sebuah paket dapat mendeklarasikan preinstall, install, postinstall, dan prepare di dalam blok scripts. npm menjalankannya selama instalasi. Skrip tersebut tidak berada dalam sandbox dan tidak ditinjau oleh siapa pun. Skrip itu adalah perintah shell yang berjalan sebagai pengguna yang mengetikkan perintah instalasi, di direktori home pengguna tersebut, dengan akses jaringan pengguna tersebut dan seluruh environment dari shell itu.
Jadi, pertanyaan yang berguna bukanlah apa yang dapat dilakukan paket tersebut. Pertanyaannya adalah file apa yang dapat dibaca pengguna itu. Pada mesin deploy biasa, jawabannya mencakup ~/.npmrc yang menyimpan token registry, ~/.ssh/id_ed25519 yang digunakan sebagai deploy key untuk SSH (secure shell), ~/.aws/credentials, ~/.docker/config.json, dan setiap variabel yang diekspor di shell. Di situlah DATABASE_URL biasanya berada.
Payload seperti ini tidak memerlukan persistence atau privilege escalation. Payload tersebut membaca beberapa file, mengirimkannya ke host melalui HTTPS, lalu keluar dengan status 0. Anda tidak melihat apa pun karena npm secara default menyembunyikan output install script. Nonaktifkan perilaku tersebut dan pantau apa yang sebenarnya berjalan:
npm ci --foreground-scriptsforeground-scripts membagikan standard input, output, dan error dengan proses npm. Dengan demikian, build script mencetak output ke terminal Anda, bukan ke buffer yang dibuang npm ketika instalasi berhasil.
Bentuk 3: typosquat dan nama yang tidak sepenuhnya Anda ketik
Typosquat adalah package yang dipublikasikan dengan nama yang mirip dengan package populer dan menunggu perintah install yang salah ketik atau salah tempel. Mekanismenya ada pada perintah, bukan pada kode. Karena itu, lockfile tidak membantu dalam kasus ini: Anda menambahkan nama yang salah satu kali, lalu lockfile akan mengunci nama tersebut dengan setia.
Varian yang lebih sering menargetkan tim daripada individu adalah dependency confusion. Package internal Anda bernama billing-utils dan berada di private registry. Jika tidak ada package bernama billing-utils di public registry, siapa pun dapat memublikasikannya. npm me-resolve nama tanpa scope terhadap default public registry, sehingga salinan publik dapat dipilih. Solusinya adalah menggunakan scope yang Anda miliki serta pemetaan registry untuk scope tersebut, di .npmrc:
@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}Sekarang @yourorg/billing-utils hanya akan diambil dari host tersebut karena pemetaan scope-ke-registry diperiksa sebelum default registry. Nama internal tanpa scope tidak memiliki pemetaan, sehingga tidak terlindungi.
Sebelum menambahkan dependency baru, periksa package tersebut, bukan badge jumlah download-nya:
npm view some-lib repository.url maintainers time.created time.modifiedPackage yang dibuat bulan lalu dan dipublikasikan oleh akun yang tidak dapat Anda kaitkan dengan public repository memiliki risiko yang berbeda dari package dengan riwayat enam tahun. Tidak satu pun fakta tersebut merupakan bukti. Namun, keduanya mudah diperiksa.
Bentuk 4: pengelola paket yang berubah tanpa pemberitahuan
Pengelola paket dapat berganti. Seseorang mengalami kelelahan, orang lain menawarkan bantuan, hak publikasi berpindah, dan tidak ada pemberitahuan apa pun yang sampai ke proyek yang bergantung padanya. Tidak ada yang disusupi. Kepercayaan yang Anda berikan pada 2021 kini berada di tangan orang yang berbeda.
Ini adalah bentuk yang paling lambat dan paling sulit dideteksi, dan tidak ada perintah yang dapat menjawabnya secara langsung. Dua hal dapat mempersempit kemungkinan ini. Periksa siapa yang dapat memublikasikan paket sebelum Anda mengadopsinya, menggunakan baris npm view di atas. Kemudian baca diff saat paket yang benar-benar Anda gunakan mengalami perubahan:
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.3Bentuk pertama hanya menampilkan nama file yang berubah. Ini cukup cepat untuk dijalankan pada setiap upgrade paket yang penting bagi Anda. Patch release yang mengubah build script, menambahkan file di root paket, atau mengedit blok scripts perlu dibaca sepenuhnya sebelum diterapkan ke server Anda.
Build dari lockfile yang telah di-commit dengan npm ci
package-lock.json mencatat versi persis setiap package dalam tree, URL sumber masing-masing package, hash integritas sha512 setiap tarball, serta package yang memerlukannya. Commit file ini. Hanya file ini yang menunjukkan hal-hal yang benar-benar telah Anda uji.
Kemudian lakukan instalasi dengan npm ci, bukan npm install, pada mesin apa pun yang bukan laptop developer:
npm ci --omit=dev --ignore-scriptsnpm ci berbeda dari npm install dalam hal-hal yang semuanya penting di sini. Perintah ini mengharuskan lockfile tersedia. Perintah ini menghapus node_modules yang sudah ada sebelum mulai, sehingga sisa dari deployment sebelumnya tidak dapat terbawa ke deployment ini. Perintah ini tidak pernah menulis ke package.json atau lockfile, sehingga instalasi tidak dapat diam-diam memindahkan Anda ke versi yang lebih baru. Jika lockfile dan package.json tidak cocok, perintah ini keluar dengan error, bukan mencoba menyelesaikan perbedaannya.
Error tersebut adalah fitur, bukan gangguan. Artinya, perubahan dependency harus masuk sebagai commit yang telah ditinjau seseorang, bukan sebagai efek samping deployment pada pukul 02:00.
Hash integritas diperiksa pada setiap pengambilan file. Jika byte tarball tidak cocok dengan hash yang tercatat, instalasi gagal dengan code EINTEGRITY dan tarball tidak diekstrak. Pahami manfaatnya secara tepat: mekanisme ini membuktikan bahwa file yang Anda terima adalah file yang dipin oleh lockfile. Jaminan ini sama dengan memverifikasi download dengan checksum, dan memiliki keterbatasan yang sama. Mekanisme ini tidak menunjukkan apakah versi yang dipin telah berbahaya sejak dipublikasikan.
Satu hal tentang --omit=dev: package tersebut tetap di-resolve dan tetap ditulis ke lockfile. Package tersebut hanya tidak ditempatkan di disk. Lebih sedikit package di disk berarti lebih sedikit install script dan lebih sedikit kode yang dimuat saat runtime, sehingga langkah ini layak dilakukan. Namun, langkah ini tidak menghapus dependency dari tree Anda.
Perlakukan skrip instalasi sebagai kode, dan pahami kapan harus menolaknya
Anda dapat menonaktifkan skrip instalasi. Tambahkan pengaturan ini ke .npmrc milik proyek, lalu commit file tersebut bersama lockfile:
ignore-scripts=true
save-exact=trueignore-scripts=true mencegah npm menjalankan skrip yang dideklarasikan dalam dependency. save-exact=true membuat npm install some-lib menulis 1.4.2 ke dalam package.json, bukan ^1.4.2, sehingga rentang versi tidak pernah masuk ke manifest secara tidak sengaja.
Pengaturan ini dapat menyebabkan beberapa hal berhenti berfungsi. Pahami dampaknya sebelum mengaktifkannya. Package yang mengompilasi native addon atau mengunduh binary siap pakai melakukan pekerjaan tersebut melalui skrip instalasi. Jika skrip dinonaktifkan, proses instalasi tetap berhasil. Kegagalan baru muncul saat runtime, berupa module yang tidak dapat memuat file binding-nya. Solusinya adalah allowlist:
npm ci --ignore-scripts
npm rebuild better-sqlite3npm rebuild <package> menjalankan skrip build untuk package tersebut saja. Dengan demikian, Anda membuat keputusan untuk setiap package, bukan memberikan izin eksekusi secara menyeluruh kepada ratusan pihak yang tidak Anda kenal.
Untuk mengetahui seberapa besar izin tersebut saat ini, minta npm menampilkannya:
npm query ":attr(scripts, [postinstall])"Perintah itu mencetak setiap package dalam tree instalasi yang memiliki skrip postinstall. Pada aplikasi umumnya, daftar ini lebih pendek daripada perkiraan banyak orang. Hal ini membuat allowlist praktis digunakan.
Pisahkan proses build dari proses yang melayani trafik
User deploy perlu menulis ke node_modules. Proses yang menjawab permintaan HTTP tidak perlu memiliki akses tersebut. Jika keduanya menggunakan akun yang sama, kode yang dijalankan selama instalasi dapat menulis ulang kode yang melayani pengguna Anda. Kode yang berjalan saat runtime juga dapat melakukan hal yang sama.
Pisahkan keduanya. Jalankan proses build sebagai satu user, layani aplikasi sebagai user lain, dan jadikan direktori yang dilayani bersifat read-only bagi akun yang menjalankan layanan:
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/nodeappSelanjutnya, biarkan systemd menegakkan aturan tersebut. Tulis /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.targetProtectSystem=strict me-mount seluruh file system sebagai read-only untuk service ini, kecuali /dev, /proc, /sys, dan apa pun yang Anda cantumkan dalam ReadWritePaths. Dengan demikian, upaya aplikasi untuk menulis ke node_modules gagal dengan EROFS: read-only file system. Anda dapat memeriksa dan mereproduksi kegagalan ini di log sendiri dalam waktu sekitar satu menit. NoExecPaths mencakup direktori upload yang dapat ditulis: service dapat menulis file di sana, tetapi kernel menolak eksekusinya. Opsi tersebut memerlukan systemd 249 atau yang lebih baru, dan Ubuntu 24.04 menyertakan versi 255.
Ada dua hal yang perlu diperhatikan dalam unit file ini. Pertama, jangan menambahkan MemoryDenyWriteExecute=yes. Opsi tersebut ada dalam sebagian besar daftar hardening systemd. Namun, opsi itu mencegah Node berjalan karena V8 mengompilasi JavaScript menjadi machine code saat runtime dan memerlukan halaman memori yang dapat ditulis sekaligus dieksekusi. Kedua, ambil path ExecStart dari command -v node. Jika Node diinstal dengan version manager, Node berada di bawah home directory milik user deploy. ProtectHome=yes kemudian menyembunyikan direktori tersebut dari service, sehingga unit langsung gagal dengan status=203/EXEC dan satu baris log yang menyatakan bahwa executable tidak dapat ditemukan.
Periksa hasilnya, jangan hanya mempercayai isi file:
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 menampilkan setiap pengaturan hardening beserta tingkat eksposurnya, sehingga Anda dapat melihat pengaturan mana yang masih menggunakan nilai default. touch seharusnya gagal dengan Permission denied karena nodeapp tidak memiliki apa pun di bawah current. Jika perintah tersebut berhasil, kepemilikan file Anda salah dan pengaturan systemd secara diam-diam menutupi kesalahan itu.
Satu catatan tentang EnvironmentFile: systemd membacanya sebagai root sebelum beralih ke User=nodeapp, sehingga file tersebut dapat root:root dengan mode 600. Aplikasi tetap menerima variabel tersebut. Siapa pun yang memiliki shell sebagai nodeapp masih dapat membacanya dari /proc/<pid>/environ. Jadi, pengaturan ini melindungi secret saat tersimpan, bukan proses yang sedang berjalan.
Jauhkan kredensial deployment dari lingkungan build
Skrip instalasi mewarisi environment. Fakta ini harus menentukan tempat Anda melakukan build.
Cara yang paling aman adalah melakukan build di tempat yang bukan server produksi, lalu menyalin direktori hasil build ke server tersebut. Mesin build hanya menyimpan token registry dengan akses baca saja. Tidak ada SSH deploy key, cloud access key, password database, atau login container registry.
npm token create --read-onlyToken dengan akses baca saja dapat mengambil package, tetapi tidak dapat melakukan publish. Jika token tersebut dicuri dari lingkungan build, dampaknya terbatas pada kemampuan mengunduh package publik.
Jika Anda harus melakukan build di server, lakukan build sebagai user deploy dengan environment yang sengaja dibatasi, dan simpan secret runtime di /etc/nodeapp/env, yang tidak dapat dibaca oleh deploy. Alasan yang sama berlaku untuk otomatisasi build yang Anda host sendiri: runner GitHub Actions yang di-host sendiri menyimpan token dan menjalankan kode arbitrer yang dipublikasikan pada setiap job. Karena itu, runner tersebut menjadi mesin dengan nilai tertinggi dalam deployment kecil. Program apa pun yang tidak Anda tulis sendiri dan menerima seluruh environment Anda termasuk dalam kategori yang sama. Karena itu, menjauhkan secret dari environment AI agent adalah masalah yang sama dengan program berbeda di tengahnya.
Pin atau vendor untuk dependensi yang tidak dapat Anda audit
Dependensi yang di-pin adalah dependensi yang versinya tidak dapat berubah tanpa commit. Lockfile yang di-commit sudah menerapkan hal tersebut untuk seluruh pohon dependensi. Dua kasus memerlukan langkah tambahan.
Dependensi transitif adalah kasus pertama. Anda tidak mengendalikan dependensi yang digunakan oleh dependensi Anda. overrides dalam package.json memaksa satu versi digunakan di seluruh pohon:
{
"overrides": {
"some-transitive-lib": "1.4.2"
}
}Jalankan npm install sekali setelah menambahkannya agar lockfile mencatat hasilnya, lalu commit kedua file tersebut.
Kasus kedua adalah package yang tidak dapat Anda audit dan tidak dapat Anda hapus. Sertakan package tersebut sebagai vendor. npm pack mengunduh tarball persis seperti yang akan disediakan oleh registry, sedangkan dependensi file: menginstalnya dari salinan Anda:
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 tersebut kini berada di repository Anda dan tidak dapat berubah tanpa sepengetahuan Anda. Namun, Anda juga bertanggung jawab atas pembaruannya untuk seterusnya. Karena itu, gunakan cara ini untuk package kecil yang sudah tidak dipelihara dan terpaksa Anda gunakan, bukan untuk web framework.
Ada juga periode jeda yang tidak memerlukan biaya:
npm install --before=2026-08-01Opsi before membangun ulang pohon dependensi hanya menggunakan versi yang diterbitkan pada atau sebelum tanggal tersebut. Atur tanggalnya satu atau dua minggu lebih awal saat Anda memperbarui dependensi. Dengan begitu, Anda melewati periode ketika rilis bermasalah sudah tersedia tetapi belum dilaporkan. Ini adalah langkah yang kasar karena juga menahan pembaruan keamanan yang valid. Gunakan cara ini untuk menyelesaikan rentang versi, tinjau perubahan yang terjadi, lalu commit lockfile.
Bagaimana cara mengetahui versi yang benar-benar di-deploy?
Lockfile di git menunjukkan apa yang seharusnya diinstal. Disk menunjukkan apa yang sudah diinstal. Hanya yang kedua yang menjadi bukti.
npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"npm ls membaca node_modules, sehingga melaporkan apa yang benar-benar ada di disk, bukan apa yang dimaksudkan oleh lockfile. Baris node -e membaca manifest yang sudah diinstal berdasarkan path. Cara ini tetap berfungsi untuk package yang kolom exports-nya memblokir impor subpath, dan mencetak satu versi tanpa gambar struktur dependency di sekelilingnya.
Untuk bagian perbandingan lainnya, baca git:
git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'Jadikan hubungan antara keduanya permanen dengan memasukkan commit ke dalam layout deploy. Release ke /srv/nodeapp/releases/<short commit sha>, lalu arahkan /srv/nodeapp/current ke sana menggunakan symlink. Jawaban untuk pertanyaan "apa yang sedang berjalan sekarang" menjadi readlink /srv/nodeapp/current, dan informasi itu tersedia pada 03:00 bagi orang yang bukan pihak yang melakukan deploy.
Terakhir, periksa apa yang dapat dijamin oleh registry:
npm audit signaturesIni memverifikasi signature registry pada package di dalam tree yang sudah diinstal, serta memverifikasi provenance attestation untuk package yang memilikinya. Provenance menghubungkan tarball yang dipublikasikan dengan build continuous integration (CI) publik yang menghasilkannya. Dengan demikian, attestation yang terverifikasi memungkinkan Anda menelusuri kode kembali ke sebuah commit, bukan ke laptop yang tidak diketahui. Cakupannya tidak universal. Karena itu, anggap attestation yang tidak ada sebagai "tidak ada informasi", bukan sebagai "package buruk".
Tindakan setelah rilis berbahaya masuk ke server
Telusuri dari proses yang berjalan, lalu tentukan proses tersebut berjalan sebagai user mana.
Jika kode berjalan selama instalasi, anggap semua data yang dapat dibaca oleh build user telah terekspos. Rotasikan registry token, SSH keys di home directory tersebut, cloud credentials, dan setiap secret yang diekspor di shell itu. Rotasi adalah satu-satunya respons yang dapat dipertanggungjawabkan, karena Anda tidak dapat membuktikan bahwa suatu file tidak pernah dibaca.
Jika kode berjalan saat runtime sebagai service account yang aksesnya dibatasi, cakupan yang dapat dijangkaunya jauh lebih kecil: environment variables milik aplikasi dan apa pun yang dapat dijangkau melalui akses jaringannya. Itulah alasan utama menjalankan service sebagai user tanpa hak istimewa pada VPS. Cara ini tidak mencegah compromise. Cara ini menentukan seberapa banyak bagian mesin yang dapat diakses oleh compromise tersebut dan apakah compromise itu tetap bertahan setelah restart.
Selanjutnya, lakukan rebuild, bukan pembersihan. Hapus node_modules, tetapkan versi package yang terdampak ke versi di bawah versi berbahaya dalam package.json, jalankan npm install satu kali untuk memperbarui lockfile, commit perubahan tersebut, lalu deploy dengan npm ci. Jangan memperbaiki tree secara langsung. Anda tidak dapat menginventarisasi semua bagian yang disentuh oleh install script.
Catat juga rentang waktunya: deploy pertama yang mungkin mengambil versi tersebut dan deploy yang menghapusnya. Rentang itu menunjukkan log milik Anda yang harus dibaca. Rentang tersebut hanya dapat ditentukan jika rilis Anda diberi nama berdasarkan commit.
Yang tidak diperbaiki oleh semua ini
Lockfile tidak membuat dependency menjadi aman. Lockfile mengubah saat Anda menerima dependency tersebut menjadi keputusan bertanggal yang telah ditinjau, bukan efek samping dari deployment. Setiap praktik di atas melakukan perubahan yang sama, dari ketidaksengajaan menjadi pilihan.
npm audit bukan pertahanan untuk kasus ini. Perintah tersebut membandingkan tree Anda dengan database kerentanan yang telah dilaporkan, sehingga hanya menemukan masalah yang sudah dipublikasikan dan diberi nama. Serangan supply chain tidak memiliki nama selama seluruh masa efektifnya. Jalankan npm audit untuk bug lama yang sudah diketahui, tetapi jangan mengharapkan perintah tersebut memberi informasi tentang rilis yang dikirim 4 jam lalu.
Mengurangi jumlah dependency Anda lebih membantu daripada alat apa pun dalam panduan ini, dan ini adalah saran yang paling tidak populer. Setiap package yang tidak Anda tambahkan berarti satu publisher tambahan yang tidak dapat menjadi sasaran phishing atas nama Anda, dan satu install script tambahan yang tidak pernah berjalan sebagai deploy user Anda.
Semua ini juga tidak khusus untuk npm. Empat pola yang sama berlaku pada PyPI, RubyGems, container image, dan package manager distribusi Anda. Dampaknya paling terlihat pada npm karena tree-nya paling dalam dan install script dijalankan secara default. Seberapa besar bagian mesin di sekitarnya yang harus Anda lindungi bergantung pada tempatnya berjalan. Hal itu merupakan bagian dari pertanyaan yang lebih luas tentang keamanan hosting VPS.
FAQ
Apakah npm ci melindungi saya dari package npm yang telah disusupi?
npm ci melindungi Anda dari perubahan versi tanpa sepengetahuan Anda. npm ci menginstal tepat apa yang dicatat oleh package-lock.json, memeriksa setiap tarball terhadap hash integritas sha512, dan keluar dengan error jika package.json dan lockfile tidak sesuai, alih-alih menyelesaikan perbedaannya. npm ci tidak menyatakan apakah versi yang dipatok aman. Jika Anda melakukan commit terhadap lockfile yang mematok versi berbahaya, npm ci akan menginstal versi tersebut dengan tepat di setiap server yang Anda kelola, setiap kali.
Apakah saya harus menetapkan ignore-scripts=true untuk semuanya?
Tetapkan, lalu buat allowlist. ignore-scripts=true dalam .npmrc milik project menghentikan script instalasi dependency agar tidak berjalan. Ini menghilangkan jalur langsung dari package yang berbahaya ke kredensial deploy user. Package yang mengompilasi native addon atau mengambil binary yang telah dibuat sebelumnya memang memerlukan script tersebut. Jika script dinonaktifkan, package itu akan gagal kemudian saat runtime karena file binding tidak ditemukan, bukan saat instalasi. Jalankan npm ci --ignore-scripts, lalu npm rebuild <package> untuk beberapa package yang Anda putuskan untuk percayai. npm query ":attr(scripts, [postinstall])" menunjukkan jumlah sebenarnya.
Bagaimana cara mengetahui versi package yang benar-benar diinstal oleh server saya?
Baca isi disk, bukan lockfile. npm ls <package> melaporkan apa yang ada di node_modules, sedangkan node -e "console.log(require('./node_modules/<package>/package.json').version)" hanya mencetak string versi. Lockfile dalam git menjawab pertanyaan yang berbeda, yaitu apa yang seharusnya diinstal. Membandingkan keduanya adalah tujuannya. Melakukan deploy ke direktori yang namanya mengikuti commit git membuat kedua informasi tersebut tetap tersedia beberapa bulan kemudian saat Anda membutuhkannya.
Apakah npm audit dapat menemukan serangan supply chain?
Tidak. npm audit mencocokkan tree Anda dengan database kerentanan yang telah dilaporkan. Jadi, perintah ini hanya menemukan masalah yang sudah dipublikasikan dan diberi identifier. Release berbahaya belum dilaporkan selama beberapa jam atau hari ketika instalasinya berisiko. npm audit signatures adalah perintah yang lebih berguna. Perintah ini memverifikasi signature registry di seluruh tree yang telah diinstal dan memeriksa attestation provenance jika publisher menyediakannya. Dengan demikian, Anda dapat mengetahui bahwa sebuah tarball berasal dari build publik, bukan dari mesin yang tidak dikenal.
Mengapa menjalankan aplikasi sebagai user tanpa hak istimewa penting jika serangan terjadi saat instalasi?
Karena kedua kegagalan tersebut memiliki jangkauan yang berbeda, dan Anda melindungi sistem dari keduanya. Kode saat instalasi berjalan sebagai deploy user dan dapat membaca SSH key, token registry, serta kredensial cloud milik user tersebut. Kode saat runtime berjalan sebagai service account. Dengan User=nodeapp, ProtectSystem=strict, dan tanpa kredensial di disk, jangkauannya terbatas pada environment aplikasi dan database milik aplikasi tersebut. Pemisahan account juga berarti proses yang melayani trafik tidak dapat menulis ulang node_modules. Dengan demikian, kompromi saat runtime hilang pada restart berikutnya dan tidak menjadi permanen.