Mục tiêu học tập
- Nắm các sự cố supply chain nổi tiếng: event-stream, ua-parser-js, node-ipc, xz utils
- Hiểu các risk pattern: typosquatting, dependency confusion, malicious update
- Biết vì sao lockfile (
package-lock.json,yarn.lock,poetry.lock,go.sum) quan trọng - Biết signed commit (GPG, Sigstore), npm provenance, signing nói chung
- Hiểu lý do vendor-a-copy, mirror registry nội bộ (Nexus, Artifactory)
- Nắm khung SLSA (Supply-chain Levels for Software Artifacts) levels 1-4
1. Supply chain attack là gì?
Định nghĩa: tấn công nhắm vào một thành phần (dependency, build tool, CI runner, registry, maintainer account) trong chuỗi cung ứng phần mềm, không nhắm trực tiếp vào target. Một dependency compromise = mọi user của library đó bị ảnh hưởng.
Vì sao supply chain ngày càng nguy hiểm
- App hiện đại có hàng ngàn transitive dependency (node_modules thường > 1000)
- Maintainer của lib quan trọng thường là 1 người vô danh (xkcd 2347)
- Update tự động (Dependabot, Renovate) merge các bản patch không ai review
- Một npm install có thể chạy
postinstallscript tuỳ ý
2. Các sự cố nổi tiếng
event-stream (2018)
2011-2018: event-stream là package phổ biến trong npm (~2 triệu download/tuần)
2018-09: Maintainer ban đầu chán, nhận offer từ user "right9ctrl" để hand over
2018-09: right9ctrl publish version mới có dependency mới: flatmap-stream
2018-10: flatmap-stream được publish (cùng người), chứa code obfuscated
2018-11: Mã độc activate nếu app có dependency "copay" (Bitcoin wallet)
→ đánh cắp private key của ví Bitcoin
2018-11: Phát hiện và rút package
Bài học: maintainer handover không có vetting + npm cho phép publish bất cứ version nào → một transitive dep có thể thay đổi hành vi gốc.
ua-parser-js (2021)
2021-10-22: Maintainer account npm bị takeover (password leak)
2021-10-22: Attacker publish 3 phiên bản malicious: 0.7.29, 0.8.0, 1.0.0
preinstall script:
- Windows: download crypto miner + password stealer
- Linux: chỉ crypto miner
2021-10-23: User report, package được unpublished sau ~4 giờ
Nhưng hàng triệu CI/install đã chạy postinstall script
Bài học: account takeover là vector chính → 2FA bắt buộc cho maintainer, và --ignore-scripts là tuyến phòng thủ.
node-ipc protestware (2022)
2022-03: Maintainer RIAEvangelist (Brandon Nozaki Miller) cập nhật node-ipc
Thêm code: nếu IP geolocation = Nga hoặc Belarus
→ overwrite mọi file trong project với emoji "❤️"
Mục đích "protest war in Ukraine"
2022-03: Hàng nghìn dev bị ảnh hưởng, không liên quan đến cuộc chiến
2022-03: GitHub assign CVE-2022-23812 — mã độc, không phải "protest"
Bài học: ngay cả maintainer "có ý tốt" cũng có thể thành vector tấn công. Lockfile + audit là phòng thủ.
xz utils backdoor (2024) — gần như thành công
2021: "Jia Tan" bắt đầu đóng góp cho xz utils (lib nén phổ biến)
2022-23: Build trust qua hàng trăm PR hợp pháp
2024-02: Maintainer chính bị áp lực để cho "Jia Tan" co-maintain
2024-02: Jia Tan publish version 5.6.0 với backdoor:
- Backdoor được inject qua build script (m4/build-to-host.m4)
- Khi liblzma.so được load bởi sshd, hook RSA_public_decrypt
- Cho phép attacker authenticate với SSH bằng chữ ký riêng
2024-03-29: Andres Freund (Microsoft) phát hiện do ssh login chậm 500ms hơn bình thường
2024-03-30: CVE-2024-3094 (CVSS 10.0)
xz 5.6.0/5.6.1 bị thu hồi khỏi mọi distro
Backdoor đã có trong Fedora Rawhide, Debian Sid, Kali → suýt vào stable
Ước tính nếu vào stable: ~50% Linux server có thể bị backdoor SSH
Bài học đáng sợ:
- Social engineering dài hạn (3 năm)
- Backdoor được inject ở build time, không thấy trong source code Git
- Phát hiện hoàn toàn nhờ luck (Andres ngẫu nhiên benchmark)
- Even SLSA Level 3+ build có thể không bắt được nếu CI itself compromised
3. Risk patterns
3a. Typosquatting
Package thật: express
Typo package: experess, expres, expressjs, express-js
Package thật: urllib3 (Python)
Typo package: urlib3, url-lib3, urllib4
Package thật: discord.js
Typo package: discord-js, discordjs, dscord.js
Attacker publish package với tên gần giống package phổ biến, hi vọng dev gõ nhầm. Khi npm install chạy, postinstall script của typo package chạy → mã độc.
3b. Dependency confusion
2021: Alex Birsan demonstrate cách attack hơn 35 công ty lớn (Apple, Microsoft, ...)
Setup:
Công ty có internal npm package: @mycompany/internal-utils (private registry)
npm config có cả public registry và internal registry
Attack:
Attacker publish package "@mycompany/internal-utils" lên npmjs.com public
Với version số CAO hơn (vd: 99.0.0)
Khi build trong company:
npm/yarn check cả 2 registry → thấy version 99.0.0 trên public cao hơn
→ install version public (malicious) thay vì internal
Phòng thủ:
- Scope (
@mycompany/) phải reserve trên npmjs.com public dù không publish - Config
.npmrcchỉ định strict registry cho mỗi scope:@mycompany:registry=https://nexus.mycompany.com/ - Dùng
--scopemirror trong Artifactory/Nexus
3c. Malicious update / maintainer takeover
Vectors:
1. Account password reuse → leak từ data breach khác → attacker login
2. Phishing email "update your npm 2FA"
3. Maintainer hand over package cho stranger (event-stream)
4. Long-con social engineering (xz utils)
5. CI token leak → attacker publish version mới
Phòng thủ phía user:
- Lockfile + không tự động update minor/patch trong production deploy
- Pin exact version (
"react": "18.2.0", không phải"^18.2.0") - Review changelog/release notes trước khi bump
- Dùng
npm audit signaturesđể verify npm provenance
4. Lockfile — không thể thiếu
Vì sao lockfile quan trọng
package.json (declared):
"lodash": "^4.17.0" ← caret: cho phép 4.x.x
package-lock.json (resolved):
"lodash": {
"version": "4.17.21",
"integrity": "sha512-v2kDEe57lecTul...",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
}
Không có lockfile:
- Hôm nay
npm install→ lodash 4.17.21 - Mai maintainer publish 4.17.22 →
npm installlấy 4.17.22 (khác build) - Reproducibility = 0, "works on my machine" trở lại
Có lockfile + integrity:
- Mọi
npm ciđều ra cùng version y hệt - Nếu tarball bị tamper (mã độc inject) → hash mismatch → install fail
Pin transitively
Lockfile pin mọi dep kể cả transitive:
package.json: chỉ khai báo top-level deps
package-lock.json: ghi mọi sub-dep với version + integrity hash
→ hàng nghìn entries
Khi sub-dep release version mới với mã độc:
npm install (without lockfile) → có thể lấy version mới
npm ci (with lockfile) → vẫn dùng version cũ đã pin
Lockfile theo từng ecosystem
| Tool | Lockfile | Lệnh strict install |
|---|---|---|
| npm | package-lock.json | npm ci |
| yarn classic | yarn.lock | yarn install --frozen-lockfile |
| yarn berry | yarn.lock | yarn install --immutable |
| pnpm | pnpm-lock.yaml | pnpm install --frozen-lockfile |
| Python pip-tools | requirements.txt (pinned) | pip install -r requirements.txt |
| Python Poetry | poetry.lock | poetry install --no-update |
| Python uv | uv.lock | uv sync --frozen |
| Go | go.sum (kèm go.mod) | go build (đã verify hash mặc định) |
| Cargo | Cargo.lock | cargo build --locked |
| Bundler | Gemfile.lock | bundle install --frozen |
| Composer | composer.lock | composer install |
Trong CI
# GitHub Actions — đúng cách
- name: Install deps (strict)
run: npm ci # ← KHÔNG dùng "npm install"
# npm ci: fail nếu lockfile out of sync, không update lockfile
5. Signed commits và artifact signing
Vì sao sign?
Default git:
Commit có field "author" và "committer" — nhưng đây là CHỈ STRING tự khai
Tôi có thể tạo commit với author "Linus Torvalds <torvalds@kernel.org>"
→ mạo danh hoàn toàn
Signed commit:
Commit kèm chữ ký số (GPG hoặc SSH key)
Verify bằng public key → đảm bảo người sở hữu private key đã sign commit này
GPG signing
# Tạo GPG key
gpg --full-generate-key
# Chọn: RSA 4096 hoặc Ed25519, expiration 1-2 năm
# List key
gpg --list-secret-keys --keyid-format=long
# /home/user/.gnupg/secring.gpg
# sec ed25519/3AA5C34371567BD2 2024-01-15
# Export public key, upload lên GitHub
gpg --armor --export 3AA5C34371567BD2
# Config git dùng key này
git config --global user.signingkey 3AA5C34371567BD2
git config --global commit.gpgsign true
git config --global tag.gpgsign true
# Commit sẽ tự sign từ giờ
git commit -m "feat: ..."
# Verify
git log --show-signature
SSH signing (Git 2.34+)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
Đơn giản hơn GPG, dùng SSH key sẵn có.
Sigstore — keyless signing
Vấn đề GPG/SSH key: key management cho hàng triệu maintainer là nightmare. Sigstore giải quyết bằng ephemeral key + OIDC:
Flow ký:
1. Maintainer login OIDC (Google/GitHub) → get JWT
2. cosign tạo ephemeral key pair (sống ~10 phút)
3. Fulcio (cert authority của Sigstore) cấp short-lived cert có embed JWT identity
4. Maintainer sign artifact bằng ephemeral key
5. Signature + cert + JWT → upload lên Rekor (transparency log, immutable)
6. Vứt private key
Verify:
- Đọc signature
- Verify cert chain về Fulcio root
- Kiểm tra Rekor log có entry tương ứng (timestamp đáng tin)
- Đọc identity từ cert: vd "ci@release.example.com from github.com/org/repo"
# Cài cosign
brew install cosign
# Sign container image
cosign sign --yes <registry>/<image>@sha256:<digest>
# Verify
cosign verify <image> \
--certificate-identity-regexp ".+@release.example.com" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
npm provenance
npm provenance (từ 2023): khi publish package từ GitHub Actions, npm CLI gọi Sigstore để sign và liên kết package với:
- Workflow YAML đã build nó
- Commit SHA
- GitHub repo
# Trong CI:
npm publish --provenance --access public
# User verify:
npm audit signatures
# → "X packages have verified registry signatures"
# → "Y packages have verified attestations"
User có thể kiểm tra tarball thực sự được build từ source code claim, không phải laptop bí ẩn của attacker.
6. Vendor a copy / mirror registry
Vendoring
Thay vì depend vào registry public:
Copy source code của dep vào repo của mình (thư mục vendor/)
Build dùng code trong vendor/ thay vì download
Pros:
- Reproducible 100% — không depend registry uptime
- Audit được toàn bộ code đang chạy
- An toàn khỏi npm unpublish, registry compromise
Cons:
- Repo lớn lên nhiều (Go vendor có thể GB)
- Update dep = update vendor folder
- Diff PR thường khó review
Go có built-in support: go mod vendor. Một số project Node dùng patch-package cho vendoring có chọn lọc.
Mirror registry nội bộ
# .npmrc
registry=https://nexus.mycompany.com/repository/npm-proxy/
//nexus.mycompany.com/:_authToken=${NEXUS_TOKEN}
Pros:
- Centralized control: 1 nơi đến vet/scan
- Cache: tránh hit rate limit của npmjs
- Audit: log mọi package download trong công ty
- Khi npmjs down → nội bộ vẫn build được
7. SLSA framework
SLSA (Supply-chain Levels for Software Artifacts) — framework do Google + OpenSSF tạo ra, định 4 level tăng dần.
Provenance là gì?
Một attestation (JSON) describe artifact được build như thế nào:
{
"subject": [{
"name": "my-app",
"digest": { "sha256": "abc123..." }
}],
"predicate": {
"buildType": "https://github.com/actions/...",
"builder": { "id": "https://github.com/actions/runner" },
"invocation": {
"configSource": {
"uri": "git+https://github.com/org/repo@refs/tags/v1.0",
"digest": { "sha1": "deadbeef..." },
"entryPoint": ".github/workflows/release.yml"
}
},
"materials": [/* deps đã dùng */]
}
}
Người dùng verify provenance để biết:
- Artifact này được build từ commit nào
- Workflow nào trigger build
- Runner nào chạy build
- Dependencies nào được dùng
Đạt SLSA L3 với GitHub Actions
# .github/workflows/release.yml
permissions:
contents: write
id-token: write # cho OIDC
packages: write
jobs:
build:
uses: slsa-framework/slsa-github-generator/.github/workflows/builder_generic_slsa3.yml@v2.0.0
with:
base64-subjects: "${{ needs.compute-hash.outputs.hash }}"
Workflow chuẩn này:
- Build artifact trong VM ephemeral
- Generate provenance signed bởi Sigstore (keyless)
- Provenance bao gồm: commit SHA, workflow ref, runner, materials
- User verify được "artifact này đến từ chính repo+workflow này"
8. Câu hỏi ôn tập
-
event-stream và ua-parser-js bị compromise bằng cơ chế khác nhau như thế nào, và bài học cho dev là gì?
Xem đáp án
event-stream (2018): maintainer chuyển giao package cho người lạ ("right9ctrl"). Người này publish version mới với một transitive dep (flatmap-stream) chứa mã độc nhắm vào users của copay Bitcoin wallet. Vector: ownership transfer không vetting.
ua-parser-js (2021): account npm của maintainer bị takeover (password leak). Attacker publish 3 phiên bản malicious với preinstall script tải crypto miner và password stealer. Vector: account compromise + thiếu 2FA.
Bài học: (1) lockfile + audit transitive dep, (2)
--ignore-scriptscho deps lạ, (3) đừng tự động update minor/patch trong production, (4) review changelog trước khi bump major, (5) khuyến khích maintainer bật 2FA (npm yêu cầu 2FA cho top package từ 2022). -
Vì sao lockfile (
package-lock.json,poetry.lock, ...) lại quan trọng đặc biệt cho security, không chỉ reproducibility?Xem đáp án
Lockfile pin exact version + integrity hash của mọi dep (cả top-level và transitive). Hệ quả security:
(1) Pin version: nếu sub-dep release version mới chứa mã độc (như event-stream), build với lockfile cũ vẫn dùng version đã pin → không bị "auto-poisoned".
(2) Integrity verification: lockfile lưu hash của tarball. Nếu registry/CDN trả về tarball khác (do compromise hoặc MITM),
npm ci/pnpm install --frozenfail với hash mismatch.(3) Reproducibility for audit: SBOM + lockfile = biết chính xác mã nào chạy trong production tại mỗi commit, cần thiết để incident response khi CVE mới được công bố.
Bắt buộc: dùng
npm ci(không phảinpm install) trong CI. -
Dependency confusion attack hoạt động thế nào, và làm sao phòng chống?
Xem đáp án
Cách hoạt động: công ty có internal package
@mycompany/utilstrong private registry. npm config có cả public + private registry. Attacker publish package cùng tên@mycompany/utilslên public npmjs.com với version cao hơn (vd99.0.0). Khinpm install, npm resolve version cao nhất → lấy version public (malicious). Alex Birsan demo cách này crack 35+ công ty lớn (2021).Phòng chống: (1) Reserve scope
@mycompany/*trên npmjs.com public dù không publish gì, (2) cấu hình.npmrcstrict per-scope registry:@mycompany:registry=https://nexus.mycompany.com/(3) dùng mirror nội bộ làm registry duy nhất (Nexus, Artifactory) — proxy public packages, không cho phép resolve trực tiếp ra public.
-
xz utils backdoor (2024) khác các sự cố trước ở điểm nào, và tại sao đáng sợ hơn?
Xem đáp án
Khác biệt: (1) Social engineering dài hạn (~3 năm) — "Jia Tan" đóng góp PR hợp pháp để xây dựng trust, sau đó được trao co-maintainer status. (2) Backdoor không nằm trong source code git mà inject ở build time qua m4 macro — nhìn source không thấy. (3) Target rất sâu: liblzma được linked bởi sshd (qua systemd notify), backdoor cho phép RSA-based SSH auth bypass.
Đáng sợ vì: (a) detected gần như hoàn toàn nhờ luck (Andres Freund benchmark sshd, thấy chậm 500ms hơn bình thường). (b) Đã có trong rolling distro (Fedora Rawhide, Debian Sid) — suýt vào stable, ảnh hưởng hàng chục triệu server. (c) SLSA L3+ build có thể không bắt được, vì build platform compromised không phải vấn đề — source code itself contained backdoor logic.
Bài học: cần (1) reproducible builds, (2) review build script không chỉ source code, (3) "burnout maintainer" là target chính — cần hỗ trợ tài chính/người cho maintainer của crit-deps (OpenSSF Alpha-Omega program).
-
Sigstore/cosign khác GPG signing như thế nào, và vì sao nó dễ áp dụng hơn ở quy mô lớn?
Xem đáp án
GPG: maintainer giữ long-term private key (1-2 năm). Phân phối public key lên keyservers/GitHub. Verifier cần biết trước key fingerprint nào tin cậy. Vấn đề: key management khó (lost key, leak, rotation), discovery của public key chậm, mỗi project tự maintain web-of-trust.
Sigstore (keyless): không có long-term private key. Mỗi lần sign: (1) login OIDC (Google/GitHub) → JWT, (2) Fulcio cấp ephemeral cert (sống ~10 phút) embed identity từ JWT, (3) sign bằng ephemeral key, (4) signature + cert + log entry → Rekor (transparency log immutable). Vứt private key sau khi sign.
Lợi ích quy mô: (1) Không có long-term key để bị steal/leak, (2) identity = OIDC subject (vd "release@npm.example.com via github.com/org/repo workflow X") dễ verify hơn key fingerprint, (3) Rekor log immutable cho phép verify "package này đã được sign tại thời điểm X", chống backdating, (4) npm provenance, GitHub container signing đã dùng Sigstore mặc định — adoption rất nhanh.
Bài tập thực hành
# 1. Tạo SBOM-like lockfile audit
cd your-project
npm ci
npm ls --all > deps-tree.txt
wc -l deps-tree.txt # số lượng dep thực tế (thường > 1000)
# 2. Tìm dep transitive với version flagged
npm audit
npm audit --audit-level=high
npm audit signatures # check npm provenance
# 3. Block scripts trong install
npm install --ignore-scripts
# Hoặc set permanently:
npm config set ignore-scripts true
# 4. Setup GPG signing cho git
gpg --full-generate-key # chọn ed25519 hoặc RSA 4096
KEY_ID=$(gpg --list-secret-keys --keyid-format=long | grep sec | awk '{print $2}' | cut -d/ -f2)
git config --global user.signingkey $KEY_ID
git config --global commit.gpgsign true
gpg --armor --export $KEY_ID # upload lên GitHub
# Commit test
git commit --allow-empty -m "test: signed commit"
git log --show-signature -1
# 5. SSH-based signing (alternative, simpler)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
# 6. Sign container image với cosign
brew install cosign
docker build -t myorg/myapp:v1 .
docker push myorg/myapp:v1
# Keyless sign (cần OIDC từ GitHub Actions hoặc local browser)
cosign sign myorg/myapp:v1
# Verify
cosign verify myorg/myapp:v1 \
--certificate-identity giangnt@guide.inc \
--certificate-oidc-issuer https://github.com/login/oauth
# 7. Block dependency confusion với .npmrc
cat > .npmrc <<EOF
@mycompany:registry=https://nexus.mycompany.com/
//nexus.mycompany.com/:_authToken=\${NEXUS_TOKEN}
EOF
Tài liệu tham khảo chính thức
- SLSA framework
- Sigstore
- npm provenance docs
- Alex Birsan — Dependency Confusion
- OpenSSF Best Practices
- xz utils backdoor (CVE-2024-3094) writeup
Tiếp theo: Ngày 17 — SBOM & Dependency Scanning