Cara Serangan Rantaian Bekalan npm Menjejaskan Pelayan
Ketahui bagaimana serangan rantaian bekalan npm menembusi aplikasi Node melalui skrip postinstall dan typosquatting. Pelajari langkah deploy untuk menyekat ancaman ini.
Apakah serangan rantaian bekalan npm pada pelayan anda
Serangan rantaian bekalan npm sampai ke pelayan anda melalui pakej yang anda pilih untuk dipasang. Tiada port terbuka yang terlibat dan tiada langkah eksploitasi. npm (node package manager) memasang kod, dan memasang kod bermakna menjalankan kod tersebut. Oleh itu, aplikasi Node yang kecil akan menarik beberapa ratus pakej yang tidak pernah anda baca, dan mana-mana pakej tersebut boleh menerbitkan versi baharu pada bila-bila masa.
Proses deploy anda mengambil versi berniat jahat kerana arahan pemasangan anda meminta versi padanan yang paling baharu. Kod tersebut kemudian berjalan dengan keistimewaan pengguna yang menjalankan pemasangan itu. Segala perkara di bawah adalah kesan daripada dua ayat tersebut.
Bentuk-bentuk ini disusun mengikut kekerapan ia menyerang seseorang yang melakukan deploy satu aplikasi Node ke satu VPS. Susunan ini bukanlah susunan yang digunakan oleh syarikat besar, kerana syarikat besar mempunyai pendaftaran dalaman, pasukan semakan, dan cermin (mirror) bagi pendaftaran awam. Anda hanya mempunyai skrip deploy.
Senario 1: akaun penyelenggara diceroboh dan menerbitkan patch
Registri npm tidak membenarkan sesiapa mengubah kandungan versi yang sudah wujud. Penyerang yang melakukan pancingan data (phishing) terhadap penyelenggara atau mencuri token penerbitan tidak boleh menulis semula 4.18.2. Mereka menerbitkan 4.18.3.
Lihat package.json anda. Baris seperti "express": "^4.18.2" tidak bermaksud versi 4.18.2. Simbol caret bermaksud "mana-mana versi 4.x pada atau melebihi versi ini", dan ~4.18.2 bermaksud "mana-mana 4.18.x". npm install menyelesaikan julat tersebut pada saat ia dijalankan, jadi commit git yang sama, jika di-deploy dua kali pada petang yang sama, boleh memasang dua set kod yang berbeza. Jurang tersebut merupakan permukaan serangan. Tiada apa-apa pada mesin anda perlu diceroboh untuk ia terbuka.
Keluaran berniat jahat biasanya dilaporkan dan ditarik balik, tetapi penarikan balik berlaku selepas orang ramai memasangnya. Sesiapa yang melakukan deployment semasa tempoh tersebut mempunyai kod itu pada cakera. Pipeline yang menyelesaikan julat pada setiap pelaksanaan memasuki tempoh tersebut secara automatik, beberapa kali seminggu, tanpa sesiapa pun yang membuat keputusan untuk melakukannya.
Bentuk 2: skrip pemasangan berjalan sebagai pengguna yang melakukan deploy
package.json sesuatu pakej boleh mengisytiharkan preinstall, install, postinstall dan prepare dalam blok scripts miliknya. npm menjalankan skrip ini semasa pemasangan. Skrip tersebut tidak disandbox dan tiada sesiapa yang menyemaknya. Ia merupakan arahan shell yang berjalan sebagai pengguna yang menaip arahan pemasangan tersebut, di dalam direktori home pengguna itu, dengan akses rangkaian pengguna tersebut serta persekitaran penuh shell berkenaan.
Oleh itu, persoalan yang berguna bukanlah apa yang boleh dilakukan oleh pakej tersebut. Persoalannya ialah apa yang boleh dibaca oleh pengguna itu. Pada mesin deploy biasa, jawapannya termasuk ~/.npmrc yang menyimpan token registri, ~/.ssh/id_ed25519 yang digunakan sebagai kunci deploy untuk SSH (secure shell), ~/.aws/credentials, ~/.docker/config.json, dan setiap pemboleh ubah yang dieksport dalam shell, iaitu tempat di mana DATABASE_URL biasanya berada.
Payload seperti ini tidak memerlukan persistensi dan tidak memerlukan peningkatan keistimewaan (privilege escalation). Ia membaca beberapa fail, menghantarnya ke hos melalui HTTPS, dan keluar dengan status 0. Anda tidak melihat apa-apa kerana npm menyembunyikan output skrip pemasangan secara lalai. Matikan tetapan tersebut dan perhatikan apa yang sebenarnya berjalan:
npm ci --foreground-scriptsforeground-scripts berkongsi input, output dan ralat standard dengan proses npm, jadi skrip binaan mencetak ke dalam terminal anda dan bukannya ke dalam penimbal (buffer) yang dibuang oleh npm apabila pemasangan berjaya.
Bentuk 3: typosquat, dan nama yang tidak tepat anda taip
Typosquat ialah pakej yang diterbitkan di bawah nama yang hampir sama dengan nama popular, menunggu arahan pemasangan yang tersilap taip atau tersilap tampal. Mekanismenya ialah arahan tersebut, bukan kodnya, jadi lockfile tidak membantu anda di sini: anda menambah nama yang salah sekali, dan mulai saat itu lockfile akan menyematkannya dengan setia.
Varian yang memerangkap pasukan dan bukannya individu ialah dependency confusion. Pakej dalaman anda dinamakan billing-utils dan berada dalam registry peribadi. Jika tiada apa-apa yang dinamakan billing-utils wujud pada registry awam, sesiapa sahaja boleh menerbitkannya. npm menyelesaikan nama tanpa skop (unscoped) terhadap registry awam lalai, jadi salinan awam boleh menang. Penyelesaiannya ialah skop yang anda miliki serta pemetaan registry untuk skop tersebut, dalam .npmrc:
@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}Kini @yourorg/billing-utils hanya akan diambil daripada hos tersebut, kerana pemetaan skop-ke-registry dirujuk sebelum registry lalai. Nama dalaman tanpa skop tidak mempunyai pemetaan, jadi ia tidak mempunyai perlindungan.
Sebelum anda menambah sebarang dependency baharu, lihat pakej tersebut dan bukannya lencana muat turunnya:
npm view some-lib repository.url maintainers time.created time.modifiedPakej yang dicipta bulan lepas, diterbitkan oleh akaun yang tidak dapat anda kaitkan dengan repositori awam, merupakan risiko yang berbeza daripada pakej yang mempunyai sejarah enam tahun. Tiada satu pun fakta tersebut merupakan bukti. Kedua-duanya murah untuk diperiksa.
Bentuk 4: dependency yang pemiliknya berubah secara senyap
Penyelenggara menyerahkan pakej kepada orang lain. Seseorang mungkin keletihan, orang asing menawarkan bantuan, hak penerbitan berpindah, dan tiada sebarang pemberitahuan sampai kepada projek yang bergantung kepadanya. Tiada apa-apa yang terjejas. Kepercayaan yang anda berikan pada tahun 2021 kini dipegang oleh orang yang berbeza.
Ini adalah bentuk yang paling perlahan dan paling sukar dikesan, dan tiada arahan yang menjawabnya secara terus. Dua perkara boleh membantu mengecilkan skopnya. Semak siapa yang boleh menerbitkan pakej sebelum anda menggunakannya, dengan baris npm view di atas. Kemudian, baca diff apabila pakej yang anda benar-benar bergantung kepadanya berpindah:
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 mencetak nama fail yang berubah, yang cukup pantas untuk dilakukan pada setiap naik taraf pakej yang penting bagi anda. Keluaran patch yang menyentuh skrip binaan, menambah fail pada root pakej, atau menyunting blok scripts wajar dibaca sepenuhnya sebelum ia sampai ke pelayan anda.
Bina daripada lockfile yang dikomit dengan npm ci
package-lock.json merekodkan versi tepat bagi setiap pakej dalam pepohon, URL asal setiap pakej, hash integriti sha512 bagi setiap tarball, dan pakej yang memerlukannya. Komit fail ini. Ia adalah satu-satunya fail yang menyatakan apa yang sebenarnya anda telah uji.
Kemudian, pasang dengan npm ci, jangan sekali-kali menggunakan npm install, pada mana-mana mesin yang bukan komputer riba pembangun:
npm ci --omit=dev --ignore-scriptsnpm ci berbeza daripada npm install dalam aspek yang penting di sini. Ia memerlukan lockfile untuk wujud. Ia memadamkan mana-mana node_modules sedia ada sebelum bermula, supaya sisa daripada atur cara terdahulu tidak terbawa ke dalam atur cara ini. Ia tidak pernah menulis ke package.json atau ke lockfile, jadi pemasangan tidak akan memajukan versi anda secara senyap. Jika lockfile dan package.json tidak sepadan, ia akan keluar dengan ralat dan bukannya cuba menyelesaikan perbezaan tersebut.
Ralat itu adalah ciri, bukan gangguan. Ia bermakna perubahan dependensi perlu tiba sebagai komit yang telah disemak oleh seseorang, bukannya sebagai kesan sampingan daripada atur cara yang dijalankan pada pukul 02:00.
Hash integriti disemak pada setiap pengambilan. Tarball yang baitnya tidak sepadan dengan hash yang direkodkan akan menyebabkan pemasangan gagal dengan code EINTEGRITY dan bukannya dinyahzip. Fahami dengan tepat apa yang anda peroleh: ia membuktikan fail yang anda terima adalah fail yang ditetapkan oleh lockfile, iaitu jaminan yang sama seperti yang diberikan oleh pengesahan muat turun dengan checksum, dan ia mempunyai had yang sama. Ia tidak menyatakan sama ada versi yang ditetapkan itu berniat jahat semasa ia diterbitkan.
Satu perincian tentang --omit=dev: pakej-pakej tersebut masih diselesaikan dan masih ditulis ke dalam lockfile. Ia cuma tidak diletakkan pada cakera. Kurang pakej pada cakera bermakna kurang skrip pemasangan dan kurang kod yang dimuatkan semasa runtime, jadi ia berbaloi untuk dilakukan. Ia tidak membuang dependensi daripada pepohon anda.
Anggap skrip pemasangan sebagai kod, dan ketahui cara untuk menolaknya
Anda boleh mematikan skrip pemasangan. Masukkan ini ke dalam .npmrc projek dan lakukan commit bersebelahan dengan lockfile:
ignore-scripts=true
save-exact=trueignore-scripts=true menghalang npm daripada menjalankan skrip yang diisytiharkan dalam dependencies. save-exact=true menyebabkan npm install some-lib menulis 1.4.2 ke dalam package.json dan bukannya ^1.4.2, supaya julat penyelesaian tidak memasuki manifes anda secara tidak sengaja.
Tindakan ini akan menyebabkan gangguan, dan anda perlu mengetahui caranya sebelum mengaktifkannya. Pakej yang menyusun (compile) addon asli atau memuat turun binari prapasang melakukan kerja tersebut dalam skrip pemasangan. Apabila skrip dimatikan, pemasangan itu sendiri berjaya dan kegagalan akan muncul kemudian, semasa runtime, sebagai modul yang tidak dapat memuatkan fail binding-nya. Penyelesaiannya ialah senarai kebenaran (allowlist):
npm ci --ignore-scripts
npm rebuild better-sqlite3npm rebuild <package> menjalankan skrip binaan untuk pakej tersebut sahaja. Anda kini telah membuat keputusan bagi setiap pakej, bukannya memberikan kebenaran pelaksanaan menyeluruh kepada beberapa ratus orang asing yang tidak pernah anda temui.
Untuk melihat betapa besarnya kebenaran tersebut pada masa ini, tanya npm:
npm query ":attr(scripts, [postinstall])"Perintah itu akan mencetak setiap pakej dalam pepohon yang dipasang yang membawa skrip postinstall. Pada aplikasi biasa, senarai tersebut lebih pendek daripada jangkaan orang ramai, itulah sebabnya senarai kebenaran ini praktikal untuk digunakan.
Asingkan binaan daripada proses yang melayani trafik
Pengguna deploy perlu mempunyai kebenaran menulis ke node_modules. Proses yang menjawab permintaan HTTP tidak perlu. Jika kedua-duanya adalah akaun yang sama, kod yang dijalankan semasa pemasangan boleh menulis semula kod yang melayani pengguna anda, dan kod yang dijalankan semasa runtime juga boleh menulis semula kod tersebut.
Asingkan kedua-duanya. Bina sebagai satu pengguna, layani sebagai pengguna lain, dan jadikan direktori yang dilayani sebagai baca-sahaja (read-only) kepada akaun pelayan:
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/nodeappKemudian, biarkan systemd menguatkuasakannya. 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 melekapkan keseluruhan sistem fail sebagai baca-sahaja untuk servis ini, kecuali /dev, /proc, /sys dan apa sahaja yang anda senaraikan dalam ReadWritePaths. Jadi, percubaan oleh aplikasi untuk menulis ke dalam node_modules akan gagal dengan EROFS: read-only file system, yang boleh anda semak dan hasilkan semula dalam log anda sendiri dalam masa seminit. NoExecPaths meliputi direktori muat naik yang boleh ditulis: servis boleh menulis fail di sana dan kernel akan menolak untuk melaksanakannya. Pilihan itu memerlukan systemd 249 atau lebih baharu, dan Ubuntu 24.04 membekalkan versi 255.
Terdapat dua perangkap dalam fail unit ini. Pertama, jangan tambah MemoryDenyWriteExecute=yes. Ia muncul dalam kebanyakan senarai pengerasan (hardening) systemd, dan ia menghalang Node daripada bermula, kerana V8 menyusun JavaScript kepada kod mesin semasa runtime dan memerlukan halaman yang boleh ditulis dan boleh dilaksanakan. Kedua, ambil laluan ExecStart daripada command -v node. Jika Node dipasang dengan pengurus versi, ia berada di bawah direktori home pengguna deploy, ProtectHome=yes kemudian menyembunyikan direktori tersebut daripada servis, dan unit tersebut gagal serta-merta dengan status=203/EXEC dan baris log yang menyatakan fail boleh laksana tidak dapat ditemui.
Semak hasilnya dan jangan sekadar mempercayai fail tersebut:
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 menyenaraikan setiap tetapan pengerasan dengan pendedahannya, supaya anda boleh melihat yang mana masih menggunakan tetapan lalai. touch sepatutnya gagal dengan Permission denied, kerana nodeapp tidak memiliki apa-apa di bawah current. Jika ia berjaya, pemilikan fail anda adalah salah dan tetapan systemd secara senyap menutup kelemahan tersebut.
Satu nota mengenai EnvironmentFile: systemd membacanya sebagai root sebelum ia bertukar kepada User=nodeapp, jadi fail itu boleh ditetapkan sebagai root:root dengan mod 600. Aplikasi masih menerima pemboleh ubah tersebut. Sesiapa yang mempunyai shell sebagai nodeapp masih boleh membacanya daripada /proc/<pid>/environ, jadi ini melindungi rahsia semasa dalam simpanan (at rest) dan bukannya proses yang sedang berjalan.
Pastikan kelayakan (credentials) penggunaan tidak berada dalam persekitaran binaan
Skrip pemasangan mewarisi persekitaran. Fakta ini seharusnya menentukan di mana anda melakukan proses binaan.
Kaedah paling kukuh adalah dengan membina perisian di lokasi selain pelayan pengeluaran (production server) dan menyalin direktori yang telah siap ke pelayan tersebut. Mesin binaan kemudiannya hanya memegang token pendaftaran baca-sahaja (read-only) dan tiada maklumat lain. Tiada kunci penggunaan SSH, tiada kunci akses awan, tiada kata laluan pangkalan data, dan tiada log masuk pendaftaran kontena.
npm token create --read-onlyToken baca-sahaja boleh mengambil pakej tetapi tidak boleh menerbitkannya. Jika token ini dicuri daripada persekitaran binaan, kerugian yang dialami hanyalah keupayaan untuk memuat turun pakej awam.
Jika anda terpaksa membina perisian pada pelayan, lakukan sebagai pengguna deploy dengan persekitaran yang sengaja dihadkan, dan simpan rahsia masa jalan (runtime secrets) dalam /etc/nodeapp/env, yang tidak boleh dibaca oleh deploy. Penaakulan yang sama terpakai pada automasi binaan yang anda hoskan sendiri: pelari GitHub Actions yang dihoskan sendiri memegang token dan melaksanakan kod terbitan sewenang-wenangnya pada setiap tugasan, yang menjadikannya mesin paling bernilai dalam penggunaan berskala kecil. Sebarang program yang bukan anda tulis tetapi menerima keseluruhan persekitaran anda tergolong dalam kategori yang sama, itulah sebabnya menjauhkan rahsia daripada persekitaran ejen AI merupakan masalah yang sama dengan program berbeza di tengah-tengahnya.
Pin atau vendor apa yang anda tidak boleh audit
Dependency yang dipin (pinned) ialah dependency yang versinya tidak boleh berubah tanpa commit. Fail lock yang telah di-commit sudah pun melakukan perkara ini untuk keseluruhan tree. Terdapat dua kes yang memerlukan tindakan lanjut.
Kes pertama ialah transitive dependencies. Anda tidak mengawal apa yang menjadi dependency kepada dependency anda. overrides dalam package.json memaksa sesuatu versi digunakan di mana-mana bahagian dalam tree tersebut:
{
"overrides": {
"some-transitive-lib": "1.4.2"
}
}Jalankan npm install sekali selepas menambahnya supaya fail lock merekodkan hasilnya, kemudian commit kedua-dua fail tersebut.
Kes kedua ialah pakej yang anda tidak boleh audit dan tidak boleh dibuang. Lakukan vendor terhadap pakej tersebut. npm pack memuat turun tarball tepat yang akan diberikan oleh registry, dan dependency file: akan dipasang daripada 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 dalam repositori anda dan tidak boleh berubah tanpa pengetahuan anda. Anda juga telah mengambil tanggungjawab untuk mengemas kininya selama-lamanya, jadi gunakan kaedah ini untuk pakej kecil yang telah ditinggalkan (abandoned) yang terpaksa anda gunakan, bukan untuk framework web anda.
Terdapat juga tempoh bertenang (cooling-off period), yang tidak memerlukan sebarang kos:
npm install --before=2026-08-01Opsyen before membina semula tree dengan hanya menggunakan versi yang diterbitkan pada atau sebelum tarikh tersebut. Tetapkan tarikh seminggu atau dua minggu ke belakang apabila anda menyegarkan semula dependency, dan anda akan melangkau tempoh di mana release yang bermasalah mungkin aktif tetapi belum dilaporkan. Ini adalah kaedah yang kasar, kerana ia juga menahan kemas kini keselamatan yang penting. Gunakannya untuk menyelesaikan julat versi, baca apa yang berubah, kemudian commit fail lock tersebut.
Bagaimanakah saya tahu versi yang sebenarnya telah dikeluarkan?
Fail kunci (lockfile) dalam git menyatakan apa yang sepatutnya dipasang. Cakera keras pula menyatakan apa yang sebenarnya dipasang. Hanya yang kedua merupakan bukti yang sah.
npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"npm ls membaca node_modules, jadi ia melaporkan apa yang secara fizikalnya ada dan bukannya apa yang disasarkan oleh fail kunci. Baris node -e membaca manifes yang dipasang mengikut laluan, yang berfungsi walaupun untuk pakej yang medan exports-nya menyekat import sublaluan, dan mencetak satu versi tanpa sebarang lukisan pepohon di sekelilingnya.
Untuk bahagian perbandingan yang satu lagi, baca git:
git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'Jadikan hubungan antara kedua-duanya kekal dengan meletakkan komit ke dalam susun atur (layout) penempatan. Keluarkan ke dalam /srv/nodeapp/releases/<short commit sha> dan halakan /srv/nodeapp/current kepadanya menggunakan symlink. Jawapan kepada "apa yang sedang berjalan sekarang" menjadi readlink /srv/nodeapp/current, dan ia tersedia pada jam 03:00 kepada sesiapa sahaja yang bukan orang yang melakukan penempatan tersebut.
Akhir sekali, semak apa yang akan disahkan oleh pendaftaran (registry):
npm audit signaturesIni mengesahkan tandatangan pendaftaran pada pakej dalam pepohon yang dipasang, dan mengesahkan pengakuan asal (provenance attestations) bagi pakej yang mempunyainya. Pengakuan asal menghubungkan tarball yang diterbitkan dengan binaan integrasi berterusan (CI) awam yang menghasilkannya, jadi pengakuan yang disahkan bermakna anda boleh menjejaki kod tersebut kembali kepada komit dan bukannya kepada komputer riba yang tidak diketahui. Liputan tidak menyeluruh, jadi anggap pengakuan yang tiada sebagai "tiada maklumat", bukannya sebagai "pakej yang rosak".
Tindakan selepas keluaran bermasalah sampai ke pelayan anda
Mulakan siasatan daripada apa yang dijalankan, dan di bawah pengguna yang mana.
Jika kod dijalankan semasa pemasangan, anggap semua yang boleh dibaca oleh pengguna binaan (build user) telah terjejas. Lakukan putaran (rotate) pada token pendaftaran, kunci SSH dalam direktori home tersebut, kelayakan awan, dan sebarang rahsia yang dieksport dalam shell itu. Putaran adalah satu-satunya tindakan yang jujur, kerana anda tidak dapat membuktikan bahawa sesuatu fail tidak dibaca.
Jika kod dijalankan semasa runtime di bawah akaun servis yang dikunci (locked-down), set yang boleh dicapai adalah jauh lebih kecil: pemboleh ubah persekitaran aplikasi itu sendiri dan apa sahaja yang boleh dicapai oleh akses rangkaiannya. Itulah hujah utama untuk menjalankan servis sebagai pengguna tanpa keistimewaan pada VPS. Ia tidak menghalang pelanggaran keselamatan. Ia menentukan berapa banyak bahagian mesin yang terjejas, dan sama ada ia bertahan selepas but semula.
Kemudian, bina semula (rebuild) dan bukannya membersihkan. Padam node_modules, tetapkan (pin) pakej yang terjejas di bawah versi yang bermasalah dalam package.json, jalankan npm install sekali untuk mengemas kini fail kunci (lockfile), lakukan commit, dan deploy dengan npm ci. Jangan baiki pepohon (tree) di tempatnya. Anda tidak dapat menyenaraikan apa yang disentuh oleh skrip pemasangan.
Catatkan juga tempoh masa tersebut: deploy pertama yang mungkin telah menarik versi itu, dan deploy yang membuangnya. Julat itu memberitahu anda log mana yang perlu dibaca, dan ia hanya boleh dijawab jika keluaran anda dinamakan berdasarkan commit.
Perkara yang tidak diselesaikan oleh semua ini
Lockfile tidak menjadikan sesuatu dependensi selamat. Ia menukarkan detik anda menerima dependensi tersebut menjadi satu keputusan yang bertarikh dan disemak, bukannya kesan sampingan daripada proses deploy. Setiap amalan di atas melakukan penukaran yang sama, daripada satu kemalangan kepada satu pilihan.
npm audit bukanlah pertahanan dalam hal ini. Ia membandingkan tree anda dengan pangkalan data kerentanan yang dilaporkan, jadi ia hanya menemui masalah yang telah diterbitkan dan dinamakan. Serangan rantaian bekalan (supply-chain attack) tidak dinamakan sepanjang tempoh ia berkesan. Jalankan npm audit untuk pepijat lama yang diketahui dan jangan harapkan apa-apa daripadanya mengenai release yang dihantar empat jam yang lalu.
Mengurangkan jumlah dependensi anda lebih membantu berbanding mana-mana alat dalam panduan ini, dan ia merupakan nasihat yang paling kurang popular diberikan oleh sesiapa pun. Setiap pakej yang tidak anda tambah bermakna seorang lagi penerbit yang tidak boleh dipancing (phished) bagi pihak anda, dan satu lagi skrip pemasangan yang tidak akan berjalan sebagai pengguna deploy anda.
Tiada satu pun daripada ini khusus untuk npm sahaja. Empat bentuk yang sama terpakai untuk PyPI, RubyGems, imej kontena dan pengurus pakej pengedaran anda. npm adalah tempat di mana ia paling ketara kerana tree-nya paling dalam dan skrip pemasangan berjalan secara lalai. Sejauh mana mesin di sekelilingnya menjadi tanggungjawab anda untuk dipertahankan bergantung pada tempat ia dijalankan, yang merupakan sebahagian daripada soalan lebih luas tentang sama ada VPS hosting adalah selamat.
FAQ
Adakah npm ci melindungi saya daripada pakej npm yang telah dikompromi?
Ia melindungi anda daripada perubahan versi tanpa pengetahuan anda. npm ci memasang apa yang direkodkan oleh package-lock.json dengan tepat, menyemak setiap tarball terhadap hash integriti sha512 miliknya, dan keluar dengan ralat jika package.json dan lockfile tidak sepadan, bukannya cuba menyelesaikan perbezaan tersebut. Ia tidak menyatakan sama ada versi yang dipinkan itu selamat. Jika anda melakukan commit pada lockfile yang meminkan versi berniat jahat, npm ci akan memasang versi tersebut dengan setia pada setiap pelayan milik anda, setiap kali ia dijalankan.
Patutkah saya menetapkan ignore-scripts=true untuk segala-galanya?
Tetapkan ia, kemudian gunakan allowlist. ignore-scripts=true dalam .npmrc projek menghalang skrip pemasangan dependensi daripada berjalan, yang membuang laluan paling terus daripada pakej berbahaya kepada kelayakan pengguna deploy anda. Pakej yang menyusun (compile) addon natif atau mengambil binari yang telah dibina memang memerlukannya, dan dengan skrip dimatikan, ia akan gagal kemudian pada masa runtime dengan ralat fail binding yang hilang, bukannya pada masa pemasangan. Jalankan npm ci --ignore-scripts, kemudian npm rebuild <package> untuk beberapa pakej yang anda putuskan untuk dipercayai. npm query ":attr(scripts, [postinstall])" menunjukkan berapa banyak pakej yang sebenarnya ada.
Bagaimanakah cara saya mengetahui versi pakej yang sebenarnya dipasang oleh pelayan saya?
Baca cakera, bukan lockfile. npm ls <package> melaporkan apa yang ada dalam node_modules, dan node -e "console.log(require('./node_modules/<package>/package.json').version)" mencetak rentetan versi sahaja. Lockfile dalam git menjawab soalan yang berbeza, iaitu apa yang sepatutnya dipasang, dan membandingkan kedua-duanya adalah tujuan utamanya. Melakukan deploy ke dalam direktori yang dinamakan sempena git commit memastikan kedua-dua jawapan tersedia berbulan-bulan kemudian, apabila anda memerlukannya.
Adakah npm audit menemui serangan rantaian bekalan (supply-chain attacks)?
Tidak. npm audit memadankan tree anda dengan pangkalan data kerentanan yang dilaporkan, jadi ia hanya menemui isu yang telah diterbitkan dan diberikan pengecam. Keluaran berniat jahat tidak dilaporkan semasa jam atau hari di mana pemasangannya menjadi penting. npm audit signatures adalah arahan yang lebih berguna: ia mengesahkan tandatangan registri merentasi tree yang dipasang dan menyemak pengesahan asal-usul (provenance attestations) di mana penerbit menghasilkannya, yang memberitahu anda bahawa tarball tersebut datang daripada binaan awam dan bukannya daripada mesin yang tidak diketahui.
Mengapa menjalankan aplikasi sebagai pengguna tanpa keistimewaan (unprivileged user) penting jika serangan berlaku pada masa pemasangan?
Kerana kedua-dua kegagalan tersebut mempunyai jangkauan yang berbeza dan anda sedang bertahan terhadap kedua-duanya. Kod masa pemasangan berjalan sebagai pengguna deploy dan boleh membaca kunci SSH, token registri, dan kelayakan awan pengguna tersebut. Kod masa runtime berjalan sebagai akaun servis, dan dengan User=nodeapp, ProtectSystem=strict serta tiada kelayakan pada cakera yang boleh dibacanya, jangkauannya terhenti pada persekitaran aplikasi itu sendiri dan pangkalan datanya. Memisahkan akaun juga bermakna proses yang melayani trafik tidak boleh menulis semula node_modules, jadi kompromi masa runtime akan hilang pada restart seterusnya dan bukannya menjadi kekal.