At 09:35 UTC on 4 August 2026, a package that had been installed roughly 620 million times in the previous month published a new major version. It was built by the project's own GitHub Actions workflow. It carried genuine npm provenance. The signed attestation binding that release to that repository at that tag is still sitting in a public transparency log, still accurate and still verifiable. It was also a worm (Snyk, Datadog Security Labs).
The mechanics are quick to state. The first malicious commit landed at 09:02:37; keyv@6.0.0 went out 33 minutes later with a single added line, a preinstall hook. Public warnings appeared around 10:18. By then the payload was already republishing other people's packages with stolen tokens at roughly one package per second per namespace, and eight organisations fell inside a 34-minute burst. The final tally: 2,234 poisoned versions across 444 package names in 12 organisations, with more than two billion monthly installs behind them (SafeDep, Aikido). My previous write-ups were all about trust boundaries that turned out to be assumptions. This one is about the machinery we built to prove trust, and what that machinery does when the attacker is standing inside it.
Start with the part that should be uncomfortable for everyone who has ever recommended provenance, including me. Nothing here was forged. npm provenance asserts a narrow, precise claim: this artifact was built by that workflow, from that repository, at that commit. Every word of that was true. The attacker had the repository, so the CI faithfully built and attested exactly what the repository said to build, and the resulting record is genuine. SafeDep put the consequence more bluntly than any vendor blog usually does: do not read valid provenance on a future release as evidence that the release is clean, because the provenance on 6.0.0 was itself genuine. In principle we all knew a signature proves origin, not intent. In practice we shipped that distinction to millions of developers as a green check mark, and a check mark is read as safety.
Then look at where trusted publishing moved the credential. Its whole purpose is to eliminate long-lived human tokens by handing publish authority to CI, and CI derives its authority from the repository. So the design hands the keys to precisely the class of attacker who compromises a maintainer's push access, which is the class that showed up here. We did not remove the credential; we relocated it from a human who might have hesitated at an unexpected 2FA prompt to a machine that never asks and never sleeps. The worm collected the leftovers too: tokens carrying 2FA-bypass, plus secrets recovered from the Actions runner's own process memory, and, in a detail I keep re-reading, a workflow it wrote itself whose only job was to dump the entire secrets context into a log it could then read (Datadog).
Here is the reframing that changed how I read the whole incident. Epidemiologists do not describe an outbreak by asking whether a pathogen is authentic. They use a reproduction number: contact rate, multiplied by transmission probability, multiplied by infectious period. Our industry has spent roughly a decade on transmission probability: signing, 2FA, attestation, reproducible builds. That is the one term a pathogen can route around simply by being genuine. Meanwhile we maximised contact rate on purpose and called it developer experience: one credential that can republish an entire namespace, automation that fires without a human present, dependency graphs with enormous fan-in. And the infectious period is still bounded by human reaction time. We spent a decade driving the first term down, we drove the second one up on purpose and called it maturity, and we left the third to whoever happened to be awake. On 4 August the third term was 34 minutes, which was long enough for 2,234 poisoned versions.
Which leads somewhere nobody says out loud: a mature release pipeline is a better host than an amateur one. A maintainer who still types the publish command on a laptop and answers a 2FA prompt is, epidemiologically, a dead end: low contact rate, a human in the loop, no automatic forwarding. The projects that did everything we told them to do, meaning automated releases, CI-held credentials and whole-namespace tooling, are the ones that conduct. We have spent ten years turning a sparse network of hand-published packages into a dense one with automatic forwarding, and we measured our progress in signatures.
None of this is a first, either. The same worm family has now run three times: the original Shai-Hulud in September 2025 (Unit 42), a second wave that November which reached 492 packages with around 132 million monthly downloads (eSentire), and now this one, which researchers tie to the same lineage and which tags the repositories it infects with the line “Shai-Hulud: Here We Go Again” across roughly 1,300 of them (Expel, nulltap). Three outbreaks, the same fundamental design, each one arriving after another round of integrity improvements. A pathogen that keeps working against a population you have been hardening for a year is telling you which term you hardened.
The second propagation channel is the one that should worry defenders most, because no supply-chain control in existence looks at it. Alongside the registry path, the worm used stolen GitHub credentials to write files across up to 50 branches per repository, including a VS Code task configured to run when the folder is opened and a Claude Code hook configured to run when a session starts (Snyk). Nothing is published. There is no version, no lockfile entry, no digest to pin, no registry to scan, no SBOM line item. Execution begins when a human opens a folder or starts an agent, and the payload travels in-band with source, through git, into branches nobody reviewed. The timing is its own commentary: npm v12, announced a month earlier on 8 July 2026, disables dependency lifecycle scripts by default (GitHub Changelog). The ecosystem shipped the correct fix for the install channel, and within a month the worm was spreading through a channel npm does not govern: the editor and the agent.
Now the asymmetry I cannot stop thinking about. Two ledgers were load-bearing in this attack, and both are append-only. Ours: a transparency log entry that will forever state, correctly, that the malicious release is authentic keyv, built by keyv's own pipeline. Append-only means we cannot un-attest it; the most we can do is publish more records next to it. Theirs: the command-and-control domain list, stored in an Ethereum mainnet contract and read through roughly 75 public RPC endpoints. There is no registrar to notify, no host to suspend and no takedown surface at all (Upwind). Both sides now anchor durability in immutable public infrastructure. But notice which assets are still revocable: dist-tags, tokens, repositories, domains, credentials. Every one of them sits on our side of the line. And the worm's persistence includes a monitor watching the stolen GitHub token, which means our own incident response is an observable event for the attacker. We made trust permanent and defence perishable.
The same inversion shows up in its fallback exfiltration: if the C2 is unreachable, the payload creates a public repository in the victim's own GitHub account and commits the loot there, with the victim's token embedded in the commit message. Your account is the attacker's content delivery network. Three write-ups ago I learned that my own telemetry could be an input; this one says my own infrastructure can be the exfil path, and my own signature can be the passport.
What actually bounds an outbreak is the third term. There is exactly one control in npm's published roadmap that shortens the infectious period rather than the transmission probability: from January 2027, 2FA-bypass tokens will no longer be able to publish directly. They will only be able to stage a publish that a human must approve. That is, in my reading, the most epidemiologically significant line in the entire npm security programme, and it arrives last, after years of integrity work. If the reproduction-number framing is right, the ordering is backwards. Integrity reduces the chance that a hostile artifact is believed. Staged publication reduces how far a believed one can travel. Only the second one ends an epidemic.
Registry-anomaly detection does exist. Socket, Phylum and OpenSSF’s package-analysis all watch publishing behaviour. But they watch the artifact: they need the tarball, and they need to look inside it. What I have not found is anyone treating the provenance feed itself as the sensor, where the worm’s shape is visible without downloading anything at all. We already run a real-time, tamper-proof, ecosystem-wide feed of who published what, from which workflow, at what second. We call it a transparency log and we file it away like a receipt drawer. A worm's shape in that feed is unmistakable: dozens of packages, across unrelated namespaces, patch versions incrementing, inter-arrival times under a second, sharing a workflow-identity pattern. That is a shape, not a signature. It needs no payload, no reverse engineering, no vendor feed, and no cooperation from the attacker, who cannot publish without writing to it. Read provenance as epidemiology rather than as receipts, and the very infrastructure that certified this worm becomes the fastest way to see the next one, plausibly minutes after the first republish and hours before any blog post. Two smaller asks follow from the same logic: publish blast-radius metadata per package, so a consumer can prefer dependencies that cannot be mass-republished by one credential; and treat repository-local agent and editor configuration as executable content: trusted per diff, never per folder, and never auto-run when HEAD has moved since the last human look.
The arc of these notes has been a slow retreat from things I thought were boundaries. Localhost was a topology, not a wall. Defence was structural, not vigilant. My own logs were an input. Finding bugs stopped being expensive. This one is the least comfortable, because it takes away the artifact I would have used to reassure myself: a valid signature and a green provenance badge were not what failed the ecosystem. They were what carried the worm. The question I now write at the top of my notes is no longer can I verify this? It is: if this is authentic and hostile, how far does it get in thirty-four minutes?
Sources / Nguồn:
- Snyk — “Inside the keyv npm supply chain compromise”: snyk.io
- Datadog Security Labs — “Worm compromises hundreds of popular npm packages”: securitylabs.datadoghq.com
- SafeDep — 2,234 poisoned versions across 444 packages and 12 organisations: safedep.io
- Upwind — the Ethereum smart-contract C2 channel: upwind.io
- Aikido — keyv and friends, propagation and scope: aikido.dev
- GitHub Changelog — npm v12 install-time security and 2FA-bypass token deprecation (8 July 2026): github.blog
- The Hacker News — worm plants Claude Code and VS Code hooks: thehackernews.com
- Expel — “ChainDrop”: this wave placed in the Shai-Hulud worm lineage: expel.com
- Unit 42 — the original Shai-Hulud npm compromise (September 2025): unit42.paloaltonetworks.com
09:35 UTC ngày 4/8/2026, một package đã được cài khoảng 620 triệu lần trong tháng trước đó phát hành một phiên bản major mới. Chính workflow GitHub Actions của dự án đã build ra nó. Nó mang provenance npm thật. Một chứng thực có ký số, gắn bản phát hành ấy với đúng kho mã ấy tại đúng tag ấy, được ghi vào một transparency log công khai, và nó vẫn nằm đó: chính xác, kiểm chứng được, vĩnh viễn. Nó cũng là một con worm (Snyk, Datadog Security Labs).
Cơ chế thì kể rất nhanh. Commit độc hại đầu tiên vào lúc 09:02:37; keyv@6.0.0 lên registry 33 phút sau đó với đúng một dòng thêm vào, một hook preinstall. Cảnh báo công khai xuất hiện quanh 10:18. Nhưng lúc ấy payload đã đang dùng token đánh cắp để tái phát hành package của người khác với tốc độ khoảng một package mỗi giây cho mỗi namespace, và tám tổ chức bị hạ trong một đợt 34 phút. Con số cuối: 2.234 phiên bản nhiễm độc trên 444 tên package thuộc 12 tổ chức, đứng sau là hơn hai tỷ lượt cài mỗi tháng (SafeDep, Aikido). Mấy bài trước của tôi đều nói về những ranh giới tin tưởng mà hóa ra chỉ là giả định. Bài này nói về chính bộ máy ta dựng lên để chứng minh sự tin tưởng, và về việc bộ máy ấy làm gì khi kẻ tấn công đang đứng bên trong nó.
Hãy bắt đầu bằng phần đáng khó chịu với bất cứ ai từng khuyên người khác dùng provenance, kể cả tôi. Ở đây không có gì bị giả mạo. Provenance của npm khẳng định một điều rất hẹp và rất chính xác: artifact này được build bởi workflow đó, từ kho mã đó, tại commit đó. Từng chữ trong câu ấy đều đúng. Kẻ tấn công có kho mã, nên CI đã trung thực build và chứng thực đúng những gì kho mã yêu cầu, và bản ghi thu được là thật. SafeDep nói thẳng hơn hầu hết blog của các hãng: đừng đọc provenance hợp lệ trên một bản phát hành tương lai như bằng chứng rằng bản đó sạch, bởi provenance trên 6.0.0 vốn dĩ là thật. Về nguyên tắc, tất cả chúng ta đều biết một chữ ký chứng minh nguồn gốc, không chứng minh ý định. Nhưng trên thực tế, ta đã đóng gói sự phân biệt đó thành một dấu tick xanh và giao cho hàng triệu lập trình viên, mà dấu tick xanh thì luôn được đọc thành “an toàn”.
Tiếp theo, hãy xem trusted publishing đã dịch chuyển chiếc chìa khóa đi đâu. Mục đích của nó là loại bỏ token dài hạn nằm trong tay con người, bằng cách giao quyền phát hành cho CI, và CI thì lấy thẩm quyền từ kho mã. Nghĩa là thiết kế ấy trao chìa khóa cho đúng loại kẻ tấn công đã chiếm được quyền push của maintainer: chính loại đã xuất hiện trong vụ này. Ta không loại bỏ chiếc chìa khóa; ta chuyển nó từ một con người có thể chột dạ trước một lời nhắc 2FA bất thường sang một cỗ máy không bao giờ hỏi và không bao giờ ngủ. Con worm còn vét cả phần còn lại: các token có quyền bỏ qua 2FA, các secret moi ra từ bộ nhớ tiến trình của runner GitHub Actions, và, chi tiết tôi phải đọc lại nhiều lần, một workflow do chính nó viết ra, với nhiệm vụ duy nhất là in toàn bộ ngữ cảnh secret vào một log mà nó đọc được sau đó (Datadog).
Và đây là góc nhìn đã thay đổi cách tôi đọc toàn bộ vụ việc. Các nhà dịch tễ không mô tả một đợt bùng phát bằng câu hỏi mầm bệnh có “thật” hay không. Họ dùng hệ số lây: tần suất tiếp xúc, nhân với xác suất truyền bệnh, nhân với thời gian còn khả năng lây. Ngành của chúng ta đã dành khoảng một thập kỷ cho xác suất truyền, ký số, 2FA, chứng thực, build tái lập, vốn là số hạng duy nhất mà mầm bệnh có thể đi vòng qua chỉ bằng cách trở thành thật. Trong khi đó ta chủ động tối đa hóa tần suất tiếp xúc và gọi nó là trải nghiệm lập trình viên: một chiếc chìa khóa tái phát hành được cả namespace, tự động hóa chạy mà không cần người có mặt, những gói mà hàng nghìn dự án khác cùng phụ thuộc vào. Còn thời gian còn khả năng lây thì vẫn bị chặn bởi tốc độ phản ứng của con người. Suốt một thập kỷ ta kéo số hạng thứ nhất xuống, ta chủ động đẩy số hạng thứ hai lên rồi gọi đó là sự trưởng thành, còn số hạng thứ ba thì để mặc cho ai đang thức. Ngày 4/8, số hạng thứ ba là 34 phút, đủ dài cho 2.234 phiên bản nhiễm độc.
Từ đó dẫn tới một điều ít ai nói ra: một quy trình phát hành “trưởng thành” là vật chủ tốt hơn một quy trình thủ công. Một maintainer vẫn gõ lệnh publish trên laptop và bấm xác nhận 2FA, về mặt dịch tễ, là một ngõ cụt, tiếp xúc thấp, có người trong vòng lặp, không có chuyển tiếp tự động. Chính những dự án đã làm đúng mọi điều ta khuyên, phát hành tự động, chìa khóa nằm trong CI, công cụ quản cả namespace, mới là những dây dẫn. Mười năm qua ta đã biến một mạng thưa gồm các package publish bằng tay thành một mạng dày đặc có chuyển tiếp tự động, và ta đo tiến bộ của mình bằng số chữ ký.
Mà đây cũng chẳng phải lần đầu. Cùng một họ worm này đã chạy ba lần: Shai-Hulud bản gốc vào tháng 9/2025 (Unit 42), một đợt thứ hai vào tháng 11 cùng năm chạm tới 492 package với khoảng 132 triệu lượt tải mỗi tháng (eSentire), và giờ là đợt này, được các nhà nghiên cứu xếp vào cùng dòng dõi, và nó đánh dấu các kho mã bị nhiễm bằng dòng chữ “Shai-Hulud: Here We Go Again” trên khoảng 1.300 kho (Expel, nulltap). Ba đợt bùng phát, cùng một thiết kế nền tảng, mỗi đợt lại tới sau một vòng cải tiến về tính toàn vẹn. Một mầm bệnh vẫn hiệu quả trên một cộng đồng mà bạn đã gia cố suốt một năm đang nói cho bạn biết bạn đã gia cố đúng số hạng nào.
Kênh lây thứ hai mới là thứ đáng lo nhất với người phòng thủ, vì không một biện pháp bảo vệ chuỗi cung ứng nào hiện nay nhìn tới nó. Song song với đường registry, con worm dùng thông tin đăng nhập GitHub đánh cắp để ghi file vào tới 50 nhánh của mỗi kho mã, trong đó có một task VS Code được cấu hình chạy khi mở thư mục, và một hook Claude Code được cấu hình chạy khi bắt đầu một phiên làm việc (Snyk). Không có gì được publish. Không có phiên bản, không có dòng nào trong lockfile, không có digest để ghim, không có registry để quét, không có mục nào trong SBOM. Việc thực thi bắt đầu khi một con người mở một thư mục hoặc khởi động một agent, và payload đi cùng mã nguồn, qua git, vào những nhánh chẳng ai review. Thời điểm của nó tự nó đã là một lời bình: npm v12, công bố trước đó một tháng vào ngày 8/7/2026, đã tắt sẵn các script vòng đời của dependency (GitHub Changelog). Hệ sinh thái đã ra đúng bản sửa cho kênh cài đặt, và trong vòng một tháng con worm chuyển sang lây qua một kênh mà npm không quản: trình soạn thảo và agent.
Giờ tới sự bất đối xứng mà tôi không sao thôi nghĩ về nó. Có hai cuốn nhật ký đóng vai trò trụ cột trong đòn tấn công này, và cả hai đều chỉ cho phép ghi thêm (append-only). Nhật ký của chúng ta: một bản ghi minh bạch sẽ mãi mãi khẳng định, một cách chính xác, rằng bản phát hành độc hại kia là keyv thật, do chính pipeline của keyv build ra. Chỉ-ghi-thêm nghĩa là ta không thể “rút lại chứng thực”; nhiều nhất ta chỉ có thể ghi thêm bản ghi mới bên cạnh. Sổ của họ: danh sách domain điều khiển, lưu trong một smart contract trên Ethereum mainnet và đọc qua khoảng 75 điểm RPC công khai, không có nhà đăng ký nào để thông báo, không có nhà cung cấp nào để đình chỉ, không có bề mặt nào để takedown (Upwind). Cả hai bên giờ đều neo độ bền của mình vào hạ tầng công khai bất biến. Nhưng hãy để ý những tài sản vẫn còn có thể thu hồi: dist-tag, token, kho mã, domain, thông tin đăng nhập, tất cả đều nằm ở phía chúng ta. Và cơ chế duy trì của con worm có cả một tiến trình canh chiếc token GitHub đánh cắp, nghĩa là chính hoạt động ứng phó sự cố của ta lại là một sự kiện mà kẻ tấn công quan sát được. Ta đã làm cho niềm tin trở nên vĩnh viễn, còn phòng thủ thì mau hỏng.
Cùng sự đảo chiều đó hiện ra trong kênh rút dữ liệu dự phòng: nếu không với tới được C2, payload tạo một kho mã công khai trong chính tài khoản GitHub của nạn nhân và commit chiến lợi phẩm vào đó, với token của nạn nhân nhúng ngay trong thông điệp commit. Tài khoản của bạn chính là mạng phân phối nội dung của kẻ tấn công. Ba bài trước tôi học được rằng telemetry của mình có thể là một đầu vào; bài này nói rằng hạ tầng của mình có thể là đường rút dữ liệu, và chữ ký của mình có thể là tấm hộ chiếu.
Vậy điều gì thực sự chặn được một đợt bùng phát? Trong lộ trình bảo mật đã công bố của npm, có đúng một biện pháp làm ngắn thời gian còn khả năng lây thay vì giảm xác suất truyền: từ tháng 1/2027, các token bỏ qua 2FA sẽ không còn publish trực tiếp được nữa, chúng chỉ có thể đưa bản phát hành vào hàng chờ để một người thật bấm duyệt. Theo cách đọc của tôi, đó là dòng quan trọng nhất về mặt dịch tễ trong toàn bộ chương trình an ninh của npm, và nó lại tới sau cùng, sau nhiều năm làm việc về tính toàn vẹn. Nếu khung hệ số lây là đúng, thì trật tự ưu tiên đang bị đảo. Tính toàn vẹn làm giảm khả năng một artifact độc hại được tin. Phát hành có phê duyệt làm giảm khoảng cách mà một artifact đã được tin có thể đi. Chỉ điều thứ hai mới kết thúc được một dịch bệnh.
Việc phát hiện bất thường trên registry thì đã có người làm: Socket, Phylum và package-analysis của OpenSSF đều theo dõi hành vi publish. Nhưng họ nhìn vào artifact, nghĩa là phải có tarball và phải mở nó ra xem. Thứ tôi chưa thấy ai làm là coi chính dòng provenance là cảm biến, nơi hình dạng của con worm hiện ra mà không cần tải về thứ gì. Chúng ta đã có sẵn một dòng dữ liệu thời gian thực, chống sửa đổi, phủ toàn hệ sinh thái, ghi lại ai đã publish cái gì, từ workflow nào, vào giây nào, ta gọi nó là transparency log, và ta cất nó đi như một ngăn kéo đựng hoá đơn. Hình dạng của một con worm trong dòng dữ liệu ấy thì không thể lẫn: hàng chục package, thuộc những namespace chẳng liên quan, số patch nhích lên, khoảng cách giữa các lần publish dưới một giây, cùng chung một khuôn nhận dạng workflow. Đó là một hình dạng, không phải một chữ ký nhận diện. Nó không cần payload, không cần dịch ngược, không cần nguồn tin của hãng nào, và cũng không cần kẻ tấn công hợp tác, vì hắn không thể publish mà không ghi vào đó. Hãy đọc provenance như dịch tễ học thay vì như hóa đơn, và chính cái hạ tầng đã chứng thực con worm này sẽ trở thành cách nhanh nhất để thấy con tiếp theo, có lẽ chỉ vài phút sau lần tái phát hành đầu tiên, và nhiều giờ trước bất kỳ bài blog nào. Cùng một logic dẫn tới hai đề xuất nhỏ hơn: công bố dữ liệu về “bán kính sát thương” cho từng package, để người dùng có thể ưu tiên những phụ thuộc không thể bị một chiếc chìa khóa tái phát hành hàng loạt; và coi cấu hình agent/editor nằm trong kho mã là nội dung thực thi được, tin theo từng diff, chưa bao giờ theo cả thư mục, và không bao giờ tự chạy khi HEAD đã dịch chuyển kể từ lần cuối có người nhìn vào.
Mạch của những ghi chép này là một cuộc rút lui chậm rãi khỏi những thứ tôi từng nghĩ là ranh giới. Localhost chỉ là một vị trí trong mạng, không phải một bức tường. Phòng thủ là chuyện cấu trúc, không phải chuyện cảnh giác. Log của chính tôi là một đầu vào. Việc tìm lỗ hổng không còn đắt đỏ nữa. Bài này là bài khó chịu nhất, vì nó lấy đi đúng cái vật tôi vẫn dùng để tự trấn an: một chữ ký hợp lệ và một dấu provenance xanh không phải là thứ đã thất bại, chúng chính là thứ đã chở con worm đi. Câu hỏi tôi viết ở đầu trang ghi chép giờ không còn là mình kiểm chứng được cái này không? Mà là: nếu nó vừa thật vừa thù địch, trong ba mươi bốn phút nó đi được tới đâu?
Sources / Nguồn:
- Snyk — “Inside the keyv npm supply chain compromise”: snyk.io
- Datadog Security Labs — “Worm compromises hundreds of popular npm packages”: securitylabs.datadoghq.com
- SafeDep — 2.234 phiên bản nhiễm độc trên 444 package thuộc 12 tổ chức: safedep.io
- Upwind — kênh điều khiển C2 đặt trong smart contract Ethereum: upwind.io
- Aikido — keyv và các package liên quan, cách lây và quy mô: aikido.dev
- GitHub Changelog — npm v12 siết bảo mật lúc cài đặt và khai tử token bỏ qua 2FA (8/7/2026): github.blog
- The Hacker News — con worm cài hook cho Claude Code và VS Code: thehackernews.com
- Expel — “ChainDrop”: đợt này được xếp vào dòng dõi worm Shai-Hulud: expel.com
- Unit 42 — vụ Shai-Hulud gốc trên npm (9/2025): unit42.paloaltonetworks.com