5 điểm bởi GN⁺ 2025-09-17 | 1 bình luận | Chia sẻ qua WhatsApp
  • Một cuộc tấn công chuỗi cung ứng đã xảy ra khi mã độc tự lây lan được chèn vào hơn 40 gói trong hệ sinh thái NPM, bao gồm @ctrl/tinycolor rất phổ biến, có thể gây lây nhiễm dây chuyền đến bí mật trong môi trường phát triển và cả thông tin xác thực CI/CD. Các phiên bản bị nhiễm đã bị gỡ khỏi npm
  • Payload của cuộc tấn công thực thi bất đồng bộ gói Webpack (bundle.js, ~3.6MB) trong quá trình cài đặt npm, đồng thời thực hiện thu thập thông tin xác thực trên diện rộng thông qua biến môi trường, hệ thống tệp và cloud SDK
  • Logic độc hại dùng NpmModule.updatePackage để ép vá và phát hành các gói khác, tạo ra sự lây lan theo tầng, đồng thời chèn workflow shai-hulud vào GitHub Actions để đánh cắp secret của tổ chức bằng toJSON(secrets)
  • Dữ liệu thu thập được bị rò rỉ thông qua việc tạo kho GitHub công khai 'Shai-Hulud', được ngụy trang như hoạt động phát triển bình thường nên rất khó phát hiện
  • Việc này được thực hiện âm thầm bằng cách truy cập token và endpoint siêu dữ liệu của AWS/GCP/Azure/NPM/GitHub, cùng hoạt động tìm kiếm bí mật dựa trên TruffleHog
  • Cần ngay lập tức gỡ các gói, dọn dẹp repository, thay toàn bộ thông tin xác thực, đồng thời kiểm tra log CloudTrail/GCP Audit, chặn webhook, và áp dụng branch protection/Secret Scanning/chính sách cooldown

Affected Packages

  • Đã ghi nhận tổng cộng 195 gói/phiên bản, tiêu biểu gồm @ctrl/tinycolor(4.1.1, 4.1.2), nhiều gói trong namespace @ctrl/, nhóm module @crowdstrike/, ngx-bootstrap/ngx-toastr/ng2-file-upload/ngx-color trên toàn bộ hệ sinh thái Angular/web UI, stack di động @nativescript-community/@nstudio/, toolchain khoa học sự sống teselagen/, cùng ember-*, koa2-swagger-ui, pm2-gelf-json, wdio-web-reporter
  • Với từng gói, cần đối chiếu chính xác đúng phiên bản với bảng trong bài gốc để xác minh chéo kỹ lưỡng xem có đang sử dụng hay không
    • Ví dụ: @ctrl/ngx-emoji-mart 9.2.1, 9.2.2, @ctrl/qbittorrent 9.7.1, 9.7.2, ngx-bootstrap 18.1.4, 19.0.3–20.0.5, ng2-file-upload 7.0.2–9.0.1 và nhiều trường hợp khác trên diện rộng

Immediate Actions Required

Identify and Remove Compromised Packages

  • Kiểm tra sự hiện diện của các gói bị nhiễm trong dự án: dùng npm ls @ctrl/tinycolor để rà soát
  • Gỡ ngay các gói bị nhiễm: thực hiện npm uninstall @ctrl/tinycolor
  • Tìm hash bundle.js đã biết để kiểm tra dấu vết cục bộ: dùng sha256sum | grep 46faab8a...
Quảng cáo

Clean Infected Repositories

  • Xóa workflow GitHub Actions độc hại: gỡ .github/workflows/shai-hulud-workflow.yml
  • Phát hiện và xóa nhánh shai-hulud đã được tạo trên remote: sau git ls-remote ... | grep shai-hulud, thực hiện git push origin --delete shai-hulud

Rotate All Credentials Immediately

  • Cần thay toàn bộ NPM token, GitHub PAT/Actions secret, SSH key, thông tin xác thực AWS/GCP/Azure, chuỗi kết nối DB, token bên thứ ba, secret CI/CD
  • Cũng cần xoay vòng toàn bộ các mục được lưu trong AWS Secrets Manager/GCP Secret Manager

Audit Cloud Infrastructure for Compromise

  • AWS: trong CloudTrail, kiểm tra thời điểm và mẫu gọi BatchGetSecretValue, ListSecrets, GetSecretValue; dùng IAM Credential Report để xác minh việc tạo hoặc sử dụng khóa bất thường
  • GCP: dùng Audit Logs để kiểm tra lịch sử truy cập Secret Manager, xác nhận có hay không sự kiện CreateServiceAccountKey

1 bình luận

 
GN⁺ 2025-09-17
Ý kiến Hacker News
  • Với tư cách là người dùng các gói được lưu trữ trên npm, tôi cảm thấy việc tự mình giám sát mọi dependency và cả dependency của dependency là điều không thực tế; tôi cũng không phải chuyên gia TypeScript/JavaScript nên nghĩ rằng khó có thể dễ dàng phát hiện mã độc mà kẻ tấn công đã giấu vào. Điều tôi đang cân nhắc gần đây là cách cập nhật theo "chế độ trì hoãn", tức chỉ cập nhật lên các phiên bản đã phát hành được một khoảng thời gian nhất định thay vì bản mới nhất. Ý tưởng là nếu một gói đã lộ diện ngoài công chúng khoảng 6 tuần thì khả năng mã độc bị phát hiện sẽ cao hơn. Dĩ nhiên đây không phải cách hoàn hảo, và sẽ tốt hơn nếu có công cụ cho phép ngoại lệ để áp dụng ngay bản cập nhật mới nhất trong trường hợp có vấn đề bảo mật.

    • Ngay trong bài cũng có nhắc đến một cách như vậy: tính năng NPM Package Cooldown Check. Nếu một phiên bản gói được phát hành trong khoảng thời gian do tổ chức cấu hình (mặc định là 2 ngày) được thêm vào pull request, bản build sẽ tự động thất bại. Phần lớn các cuộc tấn công chuỗi cung ứng bị phát hiện trong vòng 24 giờ, nên chỉ cần thời gian chờ rất ngắn cũng có thể giảm mức độ phơi nhiễm bảo mật.

    • Vì việc kiểm tra toàn bộ dependency là rất khó, tôi muốn lập luận rằng nên giảm số lượng dependency जितना có thể và chỉ dùng những gói nổi tiếng, đáng tin cậy. Thậm chí, nếu không ở trong một môi trường được kiểm soát đủ chặt để có thể tin tưởng mọi tác giả, thì duy trì một mức độ nào đó của tư duy 'not-invented-here' lại là lựa chọn khá hợp lý.

    • Tôi có thói quen pin phiên bản một cách rõ ràng trong package.json và dùng npm ci để chỉ cài các phiên bản được ghi trong package-lock.json. Tôi chạy npm audit trong CI để nhận cảnh báo nếu gói xuất hiện lỗ hổng. Làm vậy khiến các gói gần như ở trạng thái "đóng băng", và chỉ riêng tuổi đời của gói cũng làm giảm khả năng bị nhiễm mã độc.

    • Cá nhân tôi còn đi xa hơn thế: tôi chỉ cập nhật dependency khi bug thực sự ảnh hưởng đến môi trường sử dụng của mình. Ngay cả khi có lỗ hổng bảo mật, nếu nó không tác động đến tôi thì tôi cứ để đó. Phần lớn lập trình viên cập nhật dependency quá thường xuyên một cách không cần thiết; theo tôi chỉ nên làm khi thực sự cần. Nếu một gói đòi hỏi cập nhật thường xuyên hoặc quá phức tạp, tôi sẽ либо không dùng nó, hoặc "đóng băng" nó theo tiêu chí của mình.

    • Với uv của Python, cũng có thể giới hạn cập nhật theo cách tương tự. Ví dụ, lệnh uv lock --exclude-newer $(date --iso -d "2 days ago") có thể loại trừ các phiên bản được phát hành trong vòng 2 ngày gần đây.

  • Những vấn đề như thế này xảy ra vì các gói hoặc phiên bản mới không được giám sát. Giải pháp tốt nhất là vận hành tách biệt giữa một nhánh phát hành ổn định như Debian, nơi chỉ nhận bản vá bảo mật và sửa lỗi, với các nhánh testing/unstable do maintainer của gói theo dõi. Tất cả những người làm việc với kho gói mở tập trung (NPM, Python, Rust, v.v.) đều đang gặp cùng một vấn đề.

    • Có vấn đề trong văn hóa phát triển. Chính văn hóa có hàng trăm dependency (bắc cầu) rồi tự động cập nhật mà không suy nghĩ mới là vấn đề. Việc chọn phơi môi trường build/chạy của mình cho ngần ấy mã bên thứ ba đi kèm với trách nhiệm tương ứng.

    • Các bản phân phối cũng ngày càng cảm thấy gánh nặng vì số lượng gói cần duy trì. Thực ra chính vì vậy mà các hệ sinh thái theo ngôn ngữ (ví dụ CPAN, Maven, RubyGems, v.v.) mới phát triển: chỉ riêng bản phân phối Linux không thể đáp ứng được ứng dụng người dùng muốn, nên mới xuất hiện nhiều con đường như freshmeat, linuxbrew, flatpak, PPA. Tôi không nghĩ cộng đồng nào cũng có đủ năng lực để giám sát và hỗ trợ nhiều nhánh của vô số thư viện đa dạng như vậy.

    • Với tư cách là nhà phát triển Debian, tôi thấy việc phát hiện thay đổi thực sự ngày càng khó hơn vì quá nhiều "nhiễu" trước khi phản ánh mã upstream, đặc biệt là các thay đổi kiểu dáng đơn thuần hoặc cập nhật công cụ. Tôi mong những thay đổi kiểu này nên được tiết chế, trừ khi đó là refactor thực sự cần con người xem xét, sửa lỗi, bổ sung tính năng, hoặc kết quả từ công cụ nhằm tìm ra mã có thể gây vấn đề.

    • Trong Rust có một hệ thống tên là cargo vet. Các công ty như Google và Mozilla tham gia để cùng chia sẻ và xác minh gói.

    • Tôi nghĩ vẫn có cách giữ tính phi tập trung mà vẫn thêm được một số chốt an toàn. Chẳng hạn, với các gói đạt quy mô nhất định thì bắt buộc phải có phê duyệt từ hai tài khoản có 2FA, hoặc các gói phổ biến chỉ được phép upload lên npm thông qua hệ thống build tái lập được. Không cần từ bỏ hoàn toàn tính phân tán, chỉ là thêm một chút công sức với các dự án lớn.

  • Vì các cuộc tấn công chuỗi cung ứng gần đây diễn ra liên tiếp, tôi bắt đầu nghiêm túc cân nhắc việc render phía server hơn nữa, không dùng JavaScript. Nhờ HTMX, tôi nhận ra có thể đi rất xa mà không cần JavaScript. Làm vậy có lẽ còn khiến ứng dụng nhanh hơn và ổn định hơn.

    • Tôi muốn nhấn mạnh rằng môi trường JS truyền thống thực ra là sandbox an toàn nhất. Gần 30 năm qua, mã JS không đáng tin đã chạy trên hàng tỷ thiết bị, nhưng số vụ tấn công quy mô lớn thành công vào engine trình duyệt chỉ đếm trên đầu ngón tay. Trong khi đó, môi trường NodeJS và npm thì đang ở tình trạng cần được thiết kế lại toàn diện về mặt bảo mật. Những chuyện như leftpad bắt nguồn từ văn hóa đưa cả những đoạn mã cực nhỏ lên npm.

    • Thật lạ khi các cuộc thảo luận như thế này thường tự động bị thu hẹp thành vấn đề riêng của một môi trường cụ thể là JavaScript. Trên thực tế, vấn đề lớn hơn là ngay cả các biện pháp tăng cường bảo mật đã có trong npm cũng hoàn toàn không được áp dụng ở các môi trường khác như PyPI, Crates, v.v.

    • Vendoring có thể giảm mức độ phơi nhiễm, nhưng tôi không cho rằng đó là giải pháp gốc rễ. Nếu NPM thực sự coi trọng bảo mật, họ nên bắt buộc 2FA khi publish, quét gói trước khi phát hành, và thậm chí ép buộc ký bằng khóa phần cứng. semver hay CRC là không đủ. Tất cả những điều này nên được tích hợp sẵn trong hệ thống quản lý gói.

    • Thực ra đây không phải vấn đề riêng của JavaScript, mà xuất phát từ việc các lập trình viên không giám sát đủ kỹ khi thêm dependency mới. Điều này hoàn toàn có thể xảy ra tương tự trong các hệ sinh thái ngôn ngữ khác như Rust hay Go.

    • Tất cả các ngôn ngữ phụ thuộc nhiều vào package manager và có thư viện chuẩn nghèo nàn đều dễ tổn thương như nhau. Về dài hạn, tôi thấy mình nên quay lại dùng JavaScript thuần hơn. Rust cũng phụ thuộc gói rất nhiều; ngược lại, Go là ví dụ khá tiêu biểu trong vấn đề này.

  • Tôi nghĩ cần có một hệ thống cho phép theo dõi đoạn mã nhẹ dùng để ký commit/release bằng khóa đáng tin cậy, rồi cài đặt và xác minh. npm đã có cách provenance của npm dùng sigstore, nhưng có vẻ nó vẫn chưa được dùng rộng rãi và chủ yếu chỉ dừng ở mức xác minh bên phát hành.

  • Từ năm 2016, lỗ hổng này đã được báo cho NPM (khuyến cáo CERT), nhưng câu trả lời của NPM là WAI (working as intended).

    • Với ai không biết WAI nghĩa là gì: thường đó là viết tắt của “working as intended”.

    • Ngay cả khi không hề có postinstall script, tôi nghĩ chỉ cần import module trong quá trình build, khởi động server, chạy test, v.v. thì mã độc cuối cùng vẫn sẽ được thực thi. Rốt cuộc sau npm install thì sẽ luôn có lúc phải chạy gì đó thật...

  • Tôi nhớ lại một bình luận từng thấy ở đây vào thời vụ left-pad: một maintainer npm nổi tiếng có 600 gói npm và 1.200 dòng mã JavaScript. Một trường hợp tôi muốn nêu làm hình mẫu là esbuild, gần như không có dependency bên ngoài và chỉ dùng thư viện chuẩn của Go.
    Các dự án khác được gọi là "thế hệ mới" nếu nhìn chuỗi dependency thì biomejsswc cũng khá ít. Nhưng nếu xem mã nguồn Rust gốc thì biomejsswc rốt cuộc vẫn có rất nhiều dependency. Tôi đoán nếu các dự án kiểu này lan rộng thì hệ sinh thái cargo cũng sẽ đi theo vết xe đó. Nếu ai biết dự án lớn nào được viết theo phong cách chặt chẽ như esbuild thì mong được giới thiệu.

    • Một trong những lý do tôi chuyển sang Go là xu hướng thư viện purego. Thường chúng chỉ phụ thuộc vào thư viện chuẩn và golang.org/x, có thể biên dịch không cần CGO nên tính di động rất tốt. go mod vendor giúp quản lý rủi ro ngắn hạn, nhưng không phải lời giải tận gốc. Go cũng chưa cung cấp xác minh gói end-to-end như ký/xác thực khóa, nên rốt cuộc vẫn còn lỗ hổng. Đặc biệt, hiện nay có quá nhiều tập trung vào hạ tầng CI/CD; nếu có thể build và phát hành mà không phải truyền khóa ký thì bảo mật có lẽ sẽ tốt hơn. Tôi nghĩ package manager nên khuyến khích chữ ký GPG, và khi phát hành cũng nên đưa chữ ký vào cả git commit.

    • Trường hợp eslint là ví dụ điển hình khiến tôi thấy bức bối. Nếu nhìn đồ thị phụ thuộc thì nó khổng lồ, và nếu maintainer không ưu tiên cắt giảm dependency thì cuối cùng chỉ còn cách chuyển sang giải pháp khác như oxlint.

    • Câu trả lời là tự làm những tính năng dễ và giảm dependency bên ngoài. Chỉ cần làm vậy thôi thường cũng cắt được 2/3 tổng dependency. Đặc biệt, những thứ đơn giản như left-pad thì tự viết rồi giữ trong tầm tay mình bằng các unit nhỏ và test cũng không tạo thêm gánh nặng quản lý đáng kể. Nên mạnh dạn loại bỏ dependency không cần thiết.

    • Những gì xuất hiện trong root Cargo.toml của dự án Rust là dành cho toàn bộ workspace; dependency thực tế của từng crate riêng lẻ nông hơn nhiều. Phải đào sâu hơn mới biết được cấu trúc phụ thuộc thật sự.

    • Mặt trái là giờ để kiểm tra dự án JavaScript thì còn phải đọc cả Golang. Chưa kể sau post-install lại còn chạy node install.js, nên cuối cùng либо phải tin hoàn toàn, либо phải đọc hết mã.

  • Tôi không thể tin nổi là npm đến giờ vẫn mặc định chạy postinstall script của mọi dependency. Pnpm hay Bun chỉ chạy khi nằm trong danh sách cho phép, còn Composer thì hoàn toàn không chạy lifecycle script cho dependency. Tôi thấy cách đó an toàn hơn vì rủi ro mà các gói phụ thuộc đem lại trong môi trường build hay phát triển.

    • Tôi tò mò vì sao những cuộc tấn công quy mô lớn như vậy không thường xuyên được nghe thấy ở các package manager khác, ví dụ Rust với build.rs, Python, Java, v.v. Không chỉ postinstall, mà về nguyên lý thì hầu như hệ sinh thái nào cũng có thể bị như vậy, thế mà sự cố dường như lại tập trung chủ yếu ở npm.

    • Tôi có thấy mặc định của Pnpm đã đổi sang chặn script, và muốn biết phản ứng của cộng đồng ra sao, đặc biệt về trải nghiệm sử dụng, việc cho phép script, hay lạm dụng lệnh allow. Trong cộng đồng đóng gói Python cũng đang có thảo luận tương tự liên quan đến wheel variants, nên tôi muốn tham khảo kinh nghiệm từ các hệ sinh thái khác.

  • Cuộc tấn công lần này đã lan sang hơn 180 gói, xem blog của Aikido Security.

  • Tôi tò mò ai là người đầu tiên phát hiện cuộc tấn công này. Thú vị là mỗi blog lại ghi công theo cách khác nhau: Aikido nói "chúng tôi phát hiện một cuộc tấn công quy mô lớn", còn Socket, Ox, Safety, Phoenix, Semgrep, v.v. thì mỗi nơi mô tả một kiểu.

    • Tôi là Mackenzie, làm việc tại Aikido. Người đầu tiên báo cáo vụ này là nhà phát triển Daniel Pereira; anh ấy đã chuyển thông tin cho Socket, và Socket là bên đầu tiên phân tích 40 gói cùng mã độc. Sau đó Aikido phát hiện thêm 147 gói nữa cùng cả gói Crowdstrike. Trên thực tế, Step là bên đầu tiên nhận ra mã độc này là một loại worm tự lây lan. Điều thú vị là nhiều tổ chức đã độc lập đảm nhận những vai trò khác nhau.

    • Có vẻ nhiều nhà phát triển đã phát hiện gần như cùng thời điểm; Step và Socket mỗi bên lại nhắc đến những người khác nhau. Rốt cuộc, các vendor bảo mật trong ngành đã bắt được vụ này bằng những cách riêng của họ, như phân tích mã bằng AI (Socket, Aikido) hay giám sát pipeline bằng eBPF (Step).

    • Nếu đã có nhiều vendor phát hiện độc lập như vậy, tôi tự hỏi liệu họ có thể chia sẻ công nghệ đó trực tiếp với npm để chặn ngay từ đầu việc đăng ký gói độc hại hay không. Nếu làm thế thì họ sẽ không còn bán được hệ thống cảnh báo sớm này nữa, nên có lẽ họ không muốn công bố.

    • Bài OP trích nguyên văn: “@franky47 đã phát hiện hiện tượng này và ngay lập tức thông báo cho cộng đồng qua GitHub issue”.

  • Tôi thấy cái tên 'Shai Hulud' mà kẻ tấn công đặt khá dí dỏm: lấy tên một con sâu khổng lồ để đặt cho một worm thực sự. Ngay cả bundle.js cốt lõi cũng to tới 3.6MB; đến cả biến thể mã độc cũng đã phình rất "đậm chất npm".

    • Tôi có linh cảm sớm muộn gì cũng sẽ có một cuộc tấn công chuỗi cung ứng vô tình lôi kéo thêm một cuộc tấn công chuỗi cung ứng khác.

    • Mã độc cũng tuân theo định luật Moore: năm 1991 virus tequila chỉ có 2.6KB, còn giờ đã lên tới vài MB.