🔐 Bảo Mật npm: Hướng Dẫn Thực Chiến Để Tránh Supply Chain Attacks
Nguồn tổng hợp: Video của Liran Tal – Cloud Native Computing Switzerland & npm-security-best-practices trên GitHub
✍️ Bài viết này dành cho bất kỳ ai đang dùng npm, pnpm, Yarn hay Bun — từ fresher đến senior. Không cần đọc hết một lần, hãy bookmark lại để tra cứu khi cần!
😱 Chuyện gì đang xảy ra với npm?
Bạn có biết rằng mỗi lần gõ npm install, bạn đang tải về và thực thi code từ hàng trăm người lạ trên internet không?
Nghe đáng sợ đúng không? Nhưng đó là sự thật. Và những năm gần đây, các vụ tấn công vào chuỗi cung ứng phần mềm (supply chain attacks) đã trở nên ngày càng tinh vi và phổ biến hơn.
Một vài vụ đình đám bạn cần biết:
| Sự kiện | Năm | Hậu quả |
|---|---|---|
| event-stream | 2018 | Backdoor ăn cắp Bitcoin từ ví crypto |
| eslint-scope | 2018 | Tài khoản maintainer bị hack, publish code độc hại |
| colors & faker | 2022 | Maintainer cố ý phá hoại chính package của mình |
| node-ipc | 2022 | Protestware xóa file người dùng ở Nga/Belarus |
| Shai-Hulud | 2025 | Worm tự lan rộng, lây nhiễm hơn 500 npm packages |
| Nx Incident | 2025 | AI coding agent bị vũ khí hóa để phát tán malware |
| TanStack | 2026 | TanStack npm supply-chain compromise |
💡 Điểm chung của gần như tất cả các vụ tấn công? Chúng đều khai thác
postinstallscripts — đoạn code chạy tự động ngay khi bạnnpm install.
🗺️ Bản đồ các biện pháp bảo mật
Bài viết này bao gồm 14 best practices chia thành 3 nhóm:
📦 Bảo mật khi dùng npm (developer)
├── 1. Tắt post-install scripts
├── 2. Cài package có "thời gian ủ"
├── 3. Dùng công cụ kiểm tra bảo mật trước khi cài
├── 4. Ngăn lockfile injection
├── 5. Dùng npm ci thay npm install
└── 6. Không upgrade dependencies một cách mù quáng
🖥️ Bảo mật môi trường phát triển
├── 7. Không lưu secrets dưới dạng plaintext
└── 8. Làm việc trong Dev Containers
🚀 Bảo mật khi publish package (maintainer)
├── 9. Bật 2FA cho tài khoản npm
├── 10. Publish với Provenance Attestations
├── 11. Publish bằng OIDC
└── 12. Giảm thiểu dependencies
🔍 Đánh giá sức khỏe package
├── 13. Tham khảo Snyk Security Database
└── 14. Đừng tin hoàn toàn vào npmjs.org
PHẦN 1: BẢO MẬT KHI DÙNG npm
🛡️ #1 — Tắt Post-Install Scripts
Đây là biện pháp quan trọng nhất. Làm ngay hôm nay.
Hầu hết các vụ tấn công supply chain đều khai thác postinstall scripts. Đây là tính năng cho phép package chạy code tùy ý ngay sau khi được cài đặt — trước khi bạn kịp xem xét hay ngăn chặn bất cứ điều gì.
Hãy tưởng tượng: Bạn gõ npm install some-library, và ngay lập tức một đoạn script ẩn chạy ngầm, gửi toàn bộ file .env, SSH keys, và AWS credentials của bạn ra ngoài internet. Đó chính xác là cách Shai-Hulud và Nx incident hoạt động.
✅ Cách thực hiện (npm):
# Thiết lập toàn cục — áp dụng cho tất cả project trên máy bạn
npm config set ignore-scripts true
npm config set allow-git none
# Hoặc dùng flag khi install ad-hoc
npm install --ignore-scripts --allow-git=none <package-name>
⚠️ Lưu ý quan trọng: Ngay cả khi đã bật
--ignore-scripts, một dependency dạng git URL (ví dụ"pkg": "git+https://github.com/org/pkg") vẫn có thể ship kèm file.npmrcriêng để tự bật lại lifecycle scripts. Vì vậy, hãy dùng cả--allow-git=none(npm CLI 11.10.0+).
✅ Cách thực hiện (pnpm):
Tin vui: pnpm 10+ tắt postinstall scripts theo mặc định! Bạn chỉ cần whitelist các package cụ thể trong pnpm-workspace.yaml:
# pnpm-workspace.yaml
onlyBuiltDependencies:
- esbuild # cần build native bindings
- fsevents # cần trên macOS
# Nghiêm ngặt hơn: dừng CI nếu có package lạ chạy scripts
strictDepBuilds: true
💡 pnpm 10.21+ còn có
trustPolicy: no-downgrade— từ chối cài package nếu trust level của nó thấp hơn phiên bản trước (dấu hiệu tài khoản maintainer bị hack).
✅ Cách thực hiện (Bun):
Bun cũng tắt postinstall scripts theo mặc định. Bạn có thể whitelist qua trustedDependencies trong package.json.
⏳ #2 — Cài Package Có "Thời Gian Ủ" (Cooldown)
Vấn đề: Kẻ tấn công publish một phiên bản độc hại mới của package phổ biến. Nhiều developer cài ngay lập tức vì npm ưu tiên phiên bản mới nhất trong semver ranges. Cộng đồng phát hiện và report → npm unpublish package độc hại sau vài giờ. Nhưng nếu bạn đã cài trước đó thì sao?
Giải pháp: Đặt "thời gian ủ" — không cài bất kỳ package nào mới publish dưới X ngày.
✅ Cách thực hiện (npm):
# Không bao giờ cài package mới publish dưới 3 ngày
npm config set min-release-age 3
# Hoặc trong .npmrc
min-release-age=3
✅ Cách thực hiện (pnpm):
# pnpm-workspace.yaml
minimumReleaseAge: 20160 # 2 tuần (tính bằng phút)
# Cho phép một số package bypass (ví dụ type definitions)
minimumReleaseAgeExclude:
- '@types/react'
- typescript
✅ Cách thực hiện (Bun):
# bunfig.toml
[install]
minimumReleaseAge = 259200 # 3 ngày (tính bằng giây)
minimumReleaseAgeExcludes = ["@types/bun", "typescript"]
✅ Cách thực hiện (Yarn 4.10+):
# .yarnrc.yml
npmMinimalAgeGate: "3d"
npmPreapprovedPackages:
- "@types/react"
- "typescript"
🤖 Nếu bạn dùng Snyk, Dependabot, hay Renovate để tự động upgrade dependencies, chúng cũng có tính năng cooldown tích hợp sẵn. Snyk mặc định không recommend upgrade phiên bản < 21 ngày tuổi.
🔍 #3 — Dùng Công Cụ Kiểm Tra Bảo Mật Trước Khi Cài
Trước khi cài một package mới, bạn cần biết:
- Package này có bị typosquatting không? (tên gần giống package nổi tiếng)
- Có chứa known vulnerabilities không?
- Có postinstall scripts đáng ngờ không?
- Package mới publish hôm qua không?
Có hai công cụ tuyệt vời giúp bạn điều này:
🔧 Công cụ 1: npq (của chính Liran Tal)
# Cài đặt
npm install -g npq
# Dùng thay npm install
npq install express
# Muốn seamless hơn? Alias luôn
alias npm='npq-hero'
echo "alias npm='npq-hero'" >> ~/.zshrc
npq kiểm tra những gì?
| Kiểm tra | Mô tả |
|---|---|
| 🔒 Vulnerabilities | Tra cứu database CVE của Snyk |
| 📅 Package age | Flag package mới publish < 22 ngày |
| 🎯 Typosquatting | Phát hiện tên giống package nổi tiếng |
| ✍️ Registry signature | Xác minh chữ ký registry |
| 🏷️ Provenance | Kiểm tra metadata build provenance |
| ⚙️ Install scripts | Cảnh báo về postinstall scripts nguy hiểm |
| 📊 Package health | Kiểm tra README, LICENSE, downloads |
| ⛓️ Binary mới | Cảnh báo khi package thêm CLI binary mới |
| 📛 Deprecation | Thông báo package đã deprecated |
# Dùng với pnpm hoặc Bun
NPQ_PKG_MGR=pnpm npq install fastify
NPQ_PKG_MGR=bun npq install fastify
# Chỉ kiểm tra, không cài
npq install express --dry-run
🔧 Công cụ 2: sfw (Socket Firewall)
# Cài đặt
npm install -g sfw
# Dùng như một "firewall" bọc ngoài package manager
sfw npm install express
sfw pnpm add express
sfw yarn add express
Socket Firewall phân tích deep hơn: kiểm tra obfuscated code, network/filesystem access bất thường, dependency confusion attacks.
So sánh npq vs sfw:
| npq | sfw | |
|---|---|---|
| Phân tích | Checklist "marshalls" | Deep static analysis |
| Data sources | Snyk CVE database | Socket threat intelligence |
| Open source | ✅ Hoàn toàn | Partial |
| Package managers | npm, pnpm, Bun | npm, yarn, pnpm, pip, cargo |
🔒 #4 — Ngăn Lockfile Injection
Vấn đề ít được biết đến nhưng cực kỳ nguy hiểm: Trong một pull request, kẻ tấn công có thể chỉnh sửa file package-lock.json hoặc yarn.lock để:
- Thêm package độc hại vào dependency tree
- Thay đổi
resolvedURL của package hợp lệ sang URL họ kiểm soát - Set SHA512 integrity phù hợp để tránh bị phát hiện
Khi bạn merge PR và chạy npm install, bạn cài thẳng malware mà không hay biết.
✅ Cách thực hiện: Dùng lockfile-lint
# Cài
npm install --save-dev lockfile-lint
# Validate lockfile
npx lockfile-lint \
--path package-lock.json \
--type npm \
--allowed-hosts npm yarn \
--validate-https
Tích hợp vào CI/CD:
{
"scripts": {
"lint:lockfile": "lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https",
"preinstall": "npm run lint:lockfile"
}
}
💡 Tin tốt cho pnpm users: pnpm ít bị ảnh hưởng bởi lockfile injection hơn npm/yarn do kiến trúc của nó. Với pnpm 10.26+, bật thêm
blockExoticSubdeps: truetrongpnpm-workspace.yamlđể block transitive dependencies khỏi git URLs:blockExoticSubdeps: true
⚙️ #5 — Dùng npm ci Thay npm install
Điểm khác biệt quan trọng:
npm install |
npm ci |
|
|---|---|---|
| Lockfile | Có thể bỏ qua, cập nhật | Tuân thủ tuyệt đối |
| Khi có conflict | Tự resolve, cài version khác | Dừng lại và báo lỗi |
| Dùng cho | Development | CI/CD, production |
| Tốc độ | Chậm hơn | Nhanh hơn (clean install) |
# Luôn dùng npm ci trong CI/CD pipeline
npm ci
# Production only
npm ci --only=production
Tương đương cho các package manager khác:
# Yarn
yarn install --immutable --immutable-cache
# pnpm
pnpm install --frozen-lockfile
# Bun
bun install --frozen-lockfile
# Deno
deno install --frozen
📌 Nhớ luôn commit lockfiles vào version control:
package-lock.json,pnpm-lock.yaml,yarn.lock,bun.lock. Đây là "bản hợp đồng" đảm bảo mọi người trong team cài đúng version.
🚫 #6 — Đừng Upgrade Dependencies Một Cách Mù Quáng
Anti-pattern nguy hiểm mà nhiều team đang làm:
# ❌ ĐỪNG LÀM NÀY
npm update
npx npm-check-updates -u && npm install
Tại sao nguy hiểm? Vụ colors và node-ipc năm 2022 là ví dụ điển hình — maintainer cố ý phá hoại package của chính mình. Nếu bạn auto-upgrade, bạn cài ngay phiên bản phá hoại đó.
✅ Cách đúng:
# Upgrade có kiểm soát — chọn từng package
npx npm-check-updates --interactive
Hoặc dùng các bot có review process:
- Snyk Automated Dependency Update PRs
- Dependabot với cấu hình
cooldown - Renovate bot với
minimumReleaseAge
PHẦN 2: BẢO MẬT MÔI TRƯỜNG PHÁT TRIỂN
🔑 #7 — Không Lưu Secrets Dưới Dạng Plaintext
Đây là vấn đề của rất nhiều developer: file .env với nội dung như:
# ❌ CỰC KỲ NGUY HIỂM
DATABASE_PASSWORD=my-super-secret-password
AWS_SECRET_KEY=AKIAIOSFODNN7EXAMPLE
STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxx
Tại sao nguy hiểm ngay cả khi .env không được commit? Vì postinstall scripts (hoặc runtime code của package độc hại) có thể đọc process.env hoặc scan filesystem để tìm file .env — rồi gửi hết ra ngoài.
✅ Cách đúng: Dùng secret references
# ✅ Chỉ lưu "địa chỉ" của secret, không lưu giá trị thật
DATABASE_PASSWORD=op://vault/database/password
API_KEY=infisical://project/env/api-key
# Secret manager inject giá trị thật chỉ khi cần
op run -- npm start
# hoặc
op run --env-file="./.env" -- node server.js
Với cách này, ngay cả khi máy bạn bị xâm nhập, kẻ tấn công cũng chỉ lấy được "địa chỉ" chứ không phải giá trị thật của secret. Việc lấy giá trị thật cần thêm xác thực (ví dụ: Touch ID trên macOS).
🐳 #8 — Làm Việc Trong Dev Containers
Nguyên tắc: Blast radius reduction — nếu có gì đó xấu xảy ra, hãy đảm bảo thiệt hại chỉ giới hạn trong môi trường cô lập.
Khi bạn chạy npm install trực tiếp trên máy host, một package độc hại có thể:
- Đọc SSH keys của bạn (
~/.ssh/) - Lấy AWS credentials (
~/.aws/credentials) - Đọc
.envfiles của tất cả project khác - Thậm chí access các tool như Claude CLI, Cursor đang chạy trên máy bạn
Dev containers giới hạn phạm vi tấn công chỉ trong container.
✅ Cách thực hiện:
Tạo file .devcontainer/devcontainer.json:
{
"name": "Node.js Dev Container",
"image": "mcr.microsoft.com/devcontainers/javascript-node:18",
"features": {
"ghcr.io/devcontainers/features/1password:1": {}
},
"postCreateCommand": "npm ci",
"runArgs": [
"--security-opt=no-new-privileges:true",
"--cap-drop=ALL",
"--cap-add=CHOWN",
"--cap-add=SETUID",
"--cap-add=SETGID"
],
"containerEnv": {
"NODE_OPTIONS": "--disable-proto=delete"
}
}
PHẦN 3: BẢO MẬT KHI PUBLISH PACKAGE
(Dành cho những ai maintain hoặc publish npm packages)
🔐 #9 — Bật 2FA Cho Tài Khoản npm
Vụ eslint-scope năm 2018: Attacker đánh cắp credentials của developer, login vào npm account, và publish phiên bản độc hại. Hàng triệu project bị ảnh hưởng chỉ vì không có 2FA.
# Bật 2FA cho cả authentication lẫn publishing
npm profile enable-2fa auth-and-writes
# Hoặc chỉ cho authentication/profile changes
npm profile enable-2fa auth-only
Quan trọng hơn: npm đang tiến tới bắt buộc 2FA với granular tokens có thời hạn 7 ngày cho tất cả publish operations.
📜 #10 — Publish Với Provenance Attestations
Provenance attestation là bằng chứng mã hóa xác nhận:
- Package này được build từ source code nào
- Được build ở đâu (GitHub Actions, GitLab CI)
- Được build bởi workflow nào
Điều này giúp users xác minh package không bị tamper sau khi rời CI/CD.
# .github/workflows/publish.yml
permissions:
id-token: write # Bắt buộc cho provenance
steps:
- run: npm publish --provenance
Yêu cầu: npm CLI 9.5.0+ và GitHub Actions hoặc GitLab CI/CD với cloud-hosted runners.
🪪 #11 — Publish Bằng OIDC (Thay Long-Lived Tokens)
Long-lived npm tokens là rủi ro bảo mật lớn — nếu bị lộ trong CI logs hay .env files, kẻ tấn công có thể publish bất cứ lúc nào.
OIDC (Trusted Publishing) thay thế bằng short-lived tokens tự động expire sau mỗi workflow run:
# .github/workflows/publish.yml
permissions:
id-token: write # Không cần NPM_TOKEN nữa!
steps:
- run: npm publish
# npm tự lấy OIDC token từ GitHub Actions
Cách này tightly scoped — chỉ workflow cụ thể trong repo cụ thể mới được publish, không ai khác.
🌿 #12 — Giảm Thiểu Dependencies
Mỗi dependency bạn thêm = thêm attack surface. Người dùng install package của bạn cũng phải "chịu" toàn bộ transitive dependencies.
Quy tắc đơn giản: Trước khi npm install <something>, hãy hỏi: "Tôi có thực sự cần package này không, hay tôi có thể viết 3 dòng code để thay thế?"
// ❌ Thêm lodash chỉ để dùng một hàm
import _ from 'lodash';
const unique = _.uniq(array);
// ✅ Native JavaScript làm được
const unique = [...new Set(array)];
// ❌ Thêm axios cho một request đơn giản
import axios from 'axios';
const data = await axios.get(url);
// ✅ Native fetch (Node.js 18+)
const response = await fetch(url);
const data = await response.json();
PHẦN 4: ĐÁNH GIÁ SỨC KHỎE PACKAGE
🏥 #13 — Tham Khảo Snyk Security Database
Trước khi dùng một package mới, hãy tra cứu tại security.snyk.io.
Ví dụ: https://security.snyk.io/package/npm/lodash
Bạn sẽ thấy:
- Security: Known CVEs và vulnerabilities
- Popularity: Download trends theo thời gian
- Maintenance: Tần suất release, activity
- Community: Contributor activity, issue responsiveness
⚠️ #14 — Đừng Tin Hoàn Toàn Vào npmjs.org
Điều ít người biết: Website npmjs.org không hiển thị đầy đủ thông tin package. Cụ thể:
- Bỏ qua git/HTTPS-based dependencies trong
package.json - Source code hiển thị có thể khác với tarball thực tế được cài khi
npm install
Tức là bạn review code trên website, tưởng an toàn, nhưng code thực tế được cài lại khác!
✅ Cách inspect tarball thực tế:
# Xem nội dung package sẽ được cài mà không cần download
npm pack <package-name> --dry-run
# Download và inspect
npm pack <package-name>
tar -tzf <package-name>-<version>.tgz
📋 Checklist Thực Hành Ngay Hôm Nay
🚀 Làm ngay (< 5 phút)
- [ ]
npm config set ignore-scripts true— Tắt postinstall scripts - [ ]
npm config set allow-git none— Block git-based dependencies - [ ]
npm config set min-release-age 3— Cooldown 3 ngày - [ ]
npm install -g npq+alias npm='npq-hero'— Security audit tự động
📅 Làm trong tuần này
- [ ] Thêm
lockfile-lintvào CI pipeline - [ ] Chuyển CI/CD từ
npm install→npm ci - [ ] Kiểm tra
.envfiles, thay thế plaintext secrets bằng secret references - [ ] Review dependencies với
npx npm-check-updates --interactive
🔮 Làm trong tháng này
- [ ] Setup Dev Container cho project
- [ ] Bật 2FA cho tài khoản npm (nếu bạn là maintainer)
- [ ] Cấu hình trusted publishing với OIDC (nếu bạn publish packages)
- [ ] Migrate sang pnpm 10+ để tận dụng security defaults
🎯 Tổng Kết
Bảo mật npm không phải là làm một lần rồi xong. Đây là mindset và thói quen trong workflow hàng ngày.
Ba nguyên tắc cốt lõi để nhớ:
🛡️ Nguyên tắc 1: Đừng bao giờ cho phép code lạ chạy tự động trên máy bạn → Tắt postinstall scripts.
⏳ Nguyên tắc 2: Đừng cài gì vội vàng → Cooldown period + security audit trước khi install.
🔒 Nguyên tắc 3: Hạn chế blast radius → Dev containers, secrets management, principle of least privilege.
📚 Tài Nguyên Thêm
- 🎥 Video gốc: Liran Tal on NPM Package Security
- 📘 GitHub repo: lirantal/npm-security-best-practices
- 🔍 npq tool: github.com/lirantal/npq
- 🛡️ Snyk Security DB: security.snyk.io
- 📰 Node.js Security Newsletter: nodejs-security.com
- 📖 lockfile-lint: npmjs.com/package/lockfile-lint
- 🔥 Socket Firewall: socket.dev
All rights reserved