📚 Đây là Level 3 trong series phòng thủ npm. Bắt đầu với Level 1: Phòng Thủ npm Trong 30 Giây (config
.npmrccá nhân), rồi Level 2: Claude Code Hooks (enforcement ở cấp agent). Bài này cover phòng thủ team/CI. Level 4: Incident Response là lớp cuối cùng.
TL;DR — CI pipeline của bạn chạy
npm installhàng chục lần mỗi ngày. Sau khi GitHub Actions của Bitwarden bị chiếm ngày 22 tháng 4, playbook 5 lớp này khóa npm trong CI: enforcenpm ci, validate lockfile, review dependency trong PR, pin Action theo SHA, và publish với OIDC. Workflow copy-paste kèm theo. Nhảy tới Layer 1 →
📊 5 lớp bảo vệ gì:
- Layer 1:
npm ciđảm bảo install deterministic từ lockfile (không resolve ngầm)- Layer 2: lockfile-lint validate nguồn package đến từ registry tin cậy
- Layer 3: Dependency review action flag dependency mới trong PR trước khi merge
- Layer 4: SHA pinning ngăn tag Action bị compromised inject code độc
- Layer 5: OIDC trusted publishing loại bỏ npm token dài hạn khỏi CI
Đây là một CI workflow GitHub Actions điển hình. Đếm thử có bao nhiêu red flag:
# .github/workflows/ci.yml — TRƯỚC (5 vấn đề)jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # ❌ Tag có thể bị đổi - uses: actions/setup-node@v4 # ❌ Tag có thể bị đổi - run: npm install # ❌ Không phải npm ci - run: npm test - run: npm publish # ❌ Dùng NPM_TOKEN cố định env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # ❌ Token dài hạn # ❌ Không validate lockfile # ❌ Không review dependencyNgày 22 tháng 4 năm 2026, CI pipeline của Bitwarden bị chiếm. Bản @bitwarden/cli@2026.4.0 độc hại tồn tại trên npm trong 90 phút. Payload thực hiện 7 thao tác thu thập credential độc lập, harvest AWS, GCP, Azure, GitHub, npm, và SSH credential từ mọi CI runner kéo nó về (Endor Labs, 2026).
Vector tấn công? Một GitHub Action bị compromised trong publish workflow của Bitwarden. Không phải package độc. Không phải email phishing. Một CI/CD pipeline.
Đây là playbook 5 lớp đã có thể chặn nó.
Tại Sao CI Pipeline Là Attack Surface npm Lớn Nhất?
CI pipeline của bạn install dependency thường xuyên hơn bất kỳ developer nào, chạy với quyền elevated, và lưu trữ secret cascade vào production. Mùa xuân 2026, kẻ tấn công chuyển từ target package sang target CI/CD automation trực tiếp, compromised tj-actions/changed-files, trivy-action, Nx, và Bitwarden CLI (GitHub, 2026).
Đây là pattern, không phải trùng hợp. CI runner là target giá trị cao hơn laptop developer.
| Bề mặt | Máy dev | CI runner |
|---|---|---|
| Tần suất install | Vài lần/ngày | Mỗi PR, mỗi push |
| Truy cập secret | Token cá nhân | Deploy key, cloud cred, npm token |
| Cổng review | Dev thấy output | Log cuộn qua không đọc |
| Bán kính thiệt hại | Một laptop | Production, mọi consumer downstream |
Payload của Bitwarden scrape bộ nhớ CI runner để tìm GitHub token, rồi dùng token đó inject workflow độc vào repository khác. Một runner bị compromised cascade thành nhiều.
Key insight: Mùa xuân 2026 chứng kiến tấn công supply chain nhắm vào tj-actions/changed-files, Nx, trivy-action, và Bitwarden CLI, thiết lập pattern rõ ràng: kẻ tấn công nhắm vào CI/CD automation thay vì application code (GitHub Actions 2026 Security Roadmap). CI pipeline là target giá trị cao nhất trong software supply chain của bạn.
Level 1 và 2 trong series này bảo vệ máy developer. Bài này bảo vệ pipeline build và ship code.
Layer 1 — Tại Sao Nên Enforce npm ci Thay Vì npm install?
npm ci install độc quyền từ lockfile và fail nếu có bất kỳ mismatch nào. npm install lặng lẽ resolve version khác và ghi đè lockfile. Trong CI, npm install là lỗ hổng supply chain vì nó cho version mới lọt vào mà không review. OWASP liệt kê npm ci là khuyến nghị CI hardening cơ bản (OWASP, 2026).
Sự khác biệt:
| Hành vi | npm install | npm ci |
|---|---|---|
| Đọc lockfile | Có, lỏng lẻo | Có, nghiêm ngặt |
| Ghi đè lockfile | Có | Không bao giờ |
| Fail khi mismatch | Không | Có |
| Xóa node_modules | Không | Có (clean slate) |
| Resolve version mới | Có | Không bao giờ |
Khi chúng tôi chuyển CI từ npm install sang npm ci, tuần đầu tiên bắt được ba lần version bump lặng lẽ. Dependency mà trước đó tự nâng version mỗi lần build giờ fail to tiếng. Đó mới là mục đích.
CI step của bạn nên trông thế này:
- name: Install dependencies run: npm ci --ignore-scriptsCommit .npmrc ở cấp project để mọi teammate và CI runner đều inherit cùng default:
# .npmrc (commit vào repo root)ignore-scripts=truesave-exact=trueaudit-level=moderateSau install, rebuild chỉ những package cần lifecycle script:
npm rebuild sharp esbuild # Chỉ nếu project dùng chúngKey insight:
npm cienforce strict lockfile adherence và abort nếu phát hiện inconsistency, biến nó thành lệnh npm install duy nhất an toàn cho CI pipeline (OWASP NPM Security Cheat Sheet, 2026).npm installtrong CI là lỗ hổng supply chain vì nó lặng lẽ resolve và cài version không có trong lockfile đã review.
Đã dùng npm ci rồi? Tốt. Nhưng npm ci tin tưởng hoàn toàn vào lockfile. Nếu kẻ tấn công sửa lockfile trong PR, npm ci sẽ cài package độc mà không hỏi. Đó là Layer 2.
Layer 2 — Validate Lockfile Integrity Trong CI Bằng Cách Nào?
Kẻ tấn công inject package bị compromised vào lockfile qua pull request. Nếu lockfile bị sửa để thêm dependency mới hoặc đổi source URL, npm ci sẽ cài nó mà không hỏi. lockfile-lint validate rằng mọi package resolve về registry tin cậy và flag modification độc trước khi install (Snyk, 2026).
Thêm lockfile-lint làm dev dependency:
npm install --save-dev --save-exact --ignore-scripts lockfile-lintTạo config file ở repo root:
{ "path": "package-lock.json", "type": "npm", "allowedHosts": ["npm"], "allowedSchemes": ["https:"], "allowedUrls": [], "emptyHostname": false}Thêm CI step, chạy trước npm ci:
- name: Validate lockfile run: npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --allowed-schemes "https:"lockfile-lint bắt được gì:
- Package resolve về non-npm registry (server do kẻ tấn công kiểm soát)
- URL HTTP thay vì HTTPS (rủi ro man-in-the-middle)
- Hostname rỗng trong resolution URL (kỹ thuật obfuscation đã biết)
Key insight: npm lockfile có thể là điểm mù bảo mật vì
npm citin tưởng chúng hoàn toàn. Kẻ tấn công inject package bị compromised vào lockfile qua pull request, và lockfile đã sửa fetch code độc mỗi lần install sau đó (Snyk, 2026). lockfile-lint là lớp validation giữa “lockfile thay đổi” và “package được cài.”
Nhận tip npm security hàng tuần — Một email mỗi tuần. CI hardening, hook config, và phân tích tấn công thực tế. Đăng ký AI Developer Weekly →
Layer 3 — Review Dependency Mới Trước Khi Merge Bằng Cách Nào?
GitHub dependency review action scan PR diff tìm dependency mới hoặc updated, và block merge nếu có vulnerability đã biết. Cách này bắt transitive dependency bị compromised ở thời điểm PR review thay vì sau deploy. Năm 2025, hơn 454,600 package độc hại mới được phát hiện, tăng 75% so với năm trước (Sonatype, 2026).
Tạo .github/workflows/dependency-review.yml:
name: Dependency Reviewon: [pull_request]
permissions: contents: read pull-requests: write
jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 with: fail-on-severity: moderate comment-summary-in-pr: always deny-licenses: GPL-3.0, AGPL-3.0Action này bắt được gì:
- Dependency mới có CVE đã biết (fail PR check)
- Thay đổi license vi phạm policy
- Transitive dependency được thêm mà không qua review
Package plain-crypto-js mang RAT vào axios? Nó đến dưới dạng transitive dependency mà không ai chọn cài. Action này sẽ flag nó trong PR diff trước khi merge.
Với AI-assisted development, đây là bản CI tương đương của PreToolUse hook từ Level 2. Agent thêm package ở local, hook bắt nó. PR lên CI, dependency review action bắt nó lần nữa. Defense in depth.
Key insight: Hơn 454,600 package độc hại mới được phát hiện năm 2025, tăng 75% so với năm trước (Sonatype State of the Software Supply Chain 2026). GitHub dependency review action là cổng CI-level ngăn các package này vào codebase qua thay đổi dependency trong PR chưa review.
Layer 4 — Tại Sao Phải Pin GitHub Actions Theo SHA Đầy Đủ?
Version tag như @v4 có thể bị retag trỏ sang code khác bất cứ lúc nào. Các vụ tj-actions/changed-files, Nx, và Bitwarden đều exploit tag mutable hoặc sửa workflow để inject code độc vào CI pipeline. Pin theo full commit SHA đảm bảo workflow chạy đúng code bạn đã review (StepSecurity, 2026).
Trước và sau:
# TRƯỚC — tag có thể bị attacker đổi- uses: actions/checkout@v4- uses: actions/setup-node@v4
# SAU — commit SHA không thể thay đổi- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1- uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4Tìm SHA cho release tag:
# Lấy commit SHA cho tag cụ thểgit ls-remote https://github.com/actions/checkout refs/tags/v4.1.1Tự động hóa SHA pinning update với Dependabot. Thêm .github/dependabot.yml:
version: 2updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly"Dependabot submit PR khi action có release mới, kèm SHA mới. Bạn review diff, merge, xong. Không cần tra SHA thủ công.
Key insight: Roadmap bảo mật GitHub Actions 2026 giới thiệu workflow dependency locking, policy-driven execution control, scoped secret, và native egress firewall cho runner, tất cả trong public preview trong 3-9 tháng (GitHub Blog, 2026). Cho đến khi các tính năng này ship, SHA pinning với Dependabot là phòng thủ mạnh nhất có sẵn chống Actions bị compromised.
Layer 5 — Publish npm Package Mà Không Cần Token Lưu Trữ?
OIDC trusted publishing loại bỏ hoàn toàn npm token dài hạn khỏi CI. GitHub Actions xác thực với npm qua OIDC token ngắn hạn, và npm tự động tạo provenance attestation liên kết published package với đúng Git commit và workflow đã build nó (GitHub, 2026).
Lưu ý: Layer này áp dụng nếu bạn publish package lên npm. Nếu team chỉ consume package, nhảy xuống lệnh verification bên dưới.
Setup trusted publishing trong publish workflow:
name: Publishon: release: types: [published]
permissions: contents: read id-token: write # Cần cho OIDC
jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 with: node-version: 22 registry-url: "https://registry.npmjs.org" - run: npm ci --ignore-scripts - run: npm publish --provenance --access publicKhông NPM_TOKEN secret. Không stored credential. OIDC token là ngắn hạn và scope vào workflow run cụ thể.
Vụ Bitwarden exploit OIDC permission (id-token: write) kết hợp với branch protection rule không đủ. Kẻ tấn công sửa publish workflow năm lần liên tiếp để stage payload độc (Endor Labs, 2026). OIDC là tool đúng, nhưng phải đi kèm branch protection ngăn sửa workflow file mà không qua review.
Cho consumer: verify provenance của package bạn cài:
npm audit signaturesKey insight: Vụ Bitwarden exploit OIDC-based npm publishing với
id-token: writepermission sau khi kẻ tấn công sửapublish-cli.ymlworkflow năm lần liên tiếp để stage payload độc (Endor Labs, 2026). OIDC trusted publishing là thiết yếu, nhưng phải kết hợp với branch protection rule ngăn sửa workflow trái phép.
CI Workflow Hoàn Chỉnh Trông Như Thế Nào?
Cả 5 layer kết hợp thành hai GitHub Actions workflow file. Đây là workflow install-and-test với Layer 1-4:
name: CIon: [push, pull_request]
permissions: contents: read pull-requests: write
jobs: security: runs-on: ubuntu-latest steps: # Layer 4: Mọi action pin theo SHA - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 with: node-version: 22
# Layer 2: Validate lockfile trước install - name: Validate lockfile run: npx lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --allowed-schemes "https:"
# Layer 1: Deterministic install từ lockfile - name: Install dependencies run: npm ci --ignore-scriptsVà dependency review workflow cho PR (Layer 3):
name: Dependency Reviewon: [pull_request]
permissions: contents: read pull-requests: write
jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - uses: actions/dependency-review-action@4901385134134e04cec5fbe5ddfe3b2c5bd5d976 # v4.0.0 with: fail-on-severity: moderate comment-summary-in-pr: alwaysLayer 5 (publish với OIDC) đã trình bày ở section trước. Thêm nó như workflow riêng trigger khi release.
Thử ngay: Copy CI workflow ở trên vào
.github/workflows/ci.ymltrong repo của bạn. Push lên branch và mở PR. Lockfile validation và dependency review đều phải chạy. Nếu cái nào fail, bạn đã tìm ra gap trong setup hiện tại. Fix trước khi merge vào main.
Hoàn thành series phòng thủ npm. Level 1 bảo vệ laptop. Level 2 bảo vệ AI agent. Level 3 bảo vệ CI pipeline. Level 4 là runbook khi thứ gì đó vẫn lọt qua.
Ship production code với AI tool? Bức tranh bảo mật đầy đủ đi xa hơn npm. Đọc AI Coding Security Checklist 6 lớp →
FAQ
Có dùng được với pnpm và yarn không?
pnpm install --frozen-lockfile tương đương npm ci. Yarn có yarn install --immutable. lockfile-lint hỗ trợ cả ba lockfile format. Dependency review action và SHA pinning hoạt động ở cấp GitHub, không phụ thuộc package manager.
Còn private registry thì sao?
lockfile-lint hỗ trợ allowlist URL private registry trong .lockfile-lintrc.json qua field allowedUrls. npm ci hoạt động với config registry trong .npmrc. Provenance attestation hiện chỉ có cho public npm package. Với private package, tiếp tục dùng scoped npm token với expiration ngắn nhất có thể.
Pin action mà nó update liên tục thì sao?
Dùng Dependabot hoặc Renovate với SHA pinning enabled. Thêm .github/dependabot.yml với package-ecosystem: "github-actions". Chúng submit PR khi action có release mới, kèm SHA mới. Bạn review diff, approve, merge. Không cần tra SHA thủ công.
Có làm chậm CI không?
npm ci thường nhanh hơn npm install vì bỏ qua dependency resolution. lockfile-lint thêm khoảng 2 giây. Dependency review action thêm khoảng 10 giây trên PR workflow. Tổng thời gian CI thường thấp hơn với npm ci, kể cả khi thêm các security step.
Team phản đối vì phức tạp thì sao?
Bắt đầu với Layer 1 (npm ci thay vì npm install) và Layer 4 (SHA pinning với Dependabot). Hai cái này bắt attack vector phổ biến nhất với ít thay đổi workflow nhất. Layer 1 là đổi một từ trong workflow file. Layer 4 là một Dependabot config file. Thêm Layer 2, 3, 5 khi team quen.
Đọc Gì Tiếp Theo
- Phòng Thủ npm Trong 30 Giây Cho Dân Vibe Coding — Level 1 của series. Bốn dòng
.npmrcbảo vệ máy local trước khi CI chạy. - npm Token Bị Lộ. Đây Là 60 Phút Tiếp Theo. — Level 4 của series. Runbook incident response khi thứ gì đó vượt qua cả ba lớp phòng thủ.
- Checklist Bảo Mật AI Coding Mà Dev Nào Cũng Cần — Hub post kết nối 6 lớp bảo mật AI coding, từ npm config đến OAuth audit đến incident response.