On September 8, 2025, a maintainer of chalk, debug, and 16 other npm packages with a combined 2.6 billion weekly downloads clicked a password-reset link from npmjs.help — a domain registered three days earlier. The attacker walked away with valid credentials and a live TOTP code, published trojanized versions within the hour, and shipped a browser-only crypto-clipper that hooked window.ethereum to silently redirect wallet transactions. Maintainers reverted the packages within roughly two hours, but the incident — documented by Aikido, Sonatype, and Wiz — illustrates a pattern that has repeated across the registry ecosystem for years: event-stream in 2018, dependency-confusion uploads in 2021, and the self-propagating Shai-Hulud worm that CISA alerted on in September 2025. None of these attacks exploited a registry vulnerability. Every one of them exploited trust — in a maintainer's account, in a build artifact nobody diffed against source, or in a package name that looked close enough to the real thing. This post walks through how these attacks are actually built and published, and the specific telemetry signals — publish-time anomalies, install-script egress, and self-replication behavior — that separate a benign release from a compromised one.
How did the event-stream backdoor stay hidden for over two months?
The event-stream incident worked because the malicious code never appeared in the public GitHub repository at all. In November 2018, maintainer Dominic Tarr — who had stepped back from active development — handed publish rights to an unknown contributor using the handle "right9ctrl." That contributor added a new dependency, flatmap-stream@0.1.1, which contained a backdoor targeting the Copay bitcoin wallet application specifically: the payload only decrypted and executed when it detected the app's package name matched Copay, and only activated above certain wallet balance thresholds. Because the malicious logic lived in the minified, npm-published build of flatmap-stream rather than in any commit visible on GitHub, code review of the source tree found nothing to flag. It took over two months and a computer science student examining the published bundle directly to surface it, as reported by Snyk and the npm security team at the time. The lesson that still holds: what ships to the registry and what sits in the public repository are not guaranteed to be the same artifact, and conditional, target-specific triggers are built specifically to survive casual testing.
Why did the chalk/debug compromise spread to 18 packages at once?
It spread because the attacker compromised a person, not a package. Account takeover via phishing gives an attacker publish rights across every package that maintainer owns, and the September 2025 incident hit chalk, debug, ansi-styles, and 15 others in a single credential-theft event, according to write-ups from Aikido Security, Sonatype, and Semgrep. This is structurally different from a single-package supply chain compromise: defenders scanning for "is package X safe" miss the blast radius entirely, because the actual unit of compromise is the npm account, and any package that account can publish to is now suspect simultaneously. The payload itself was narrowly scoped — a browser-only script that intercepted window.ethereum calls to reroute cryptocurrency transactions — which meant server-side Node.js consumers of these packages were not directly targeted, but the sheer download volume (2.6 billion/week combined) meant the exposure window mattered more than the payload's sophistication.
What made Shai-Hulud different from a normal malicious package?
Shai-Hulud, first documented by security researchers and flagged in a CISA alert on September 23, 2025, was a worm — it didn't just steal credentials, it used them to republish itself. The first wave, discovered around September 15, 2025, compromised more than 500 npm packages by harvesting npm publish tokens, GitHub personal access tokens, and AWS/GCP/Azure credentials from infected build environments, then using the stolen npm tokens to inject the same malicious payload into other packages the victim maintained — a self-propagating chain rather than a single static payload. Datadog Security Labs and Unit 42 both tracked victims' private GitHub repositories being flipped to public as part of the exfiltration mechanism. A second wave — "Shai-Hulud 2.0," identified November 24, 2025 — scaled dramatically, touching tens of thousands of GitHub repositories across roughly 350 compromised accounts, per Checkmarx research. Worm behavior changes the defensive math: containment isn't "remove the bad version," it's "assume every credential the infected environment could reach is burned."
Why is account takeover the dominant entry point, not registry exploits?
Across event-stream, chalk/debug, and Shai-Hulud, the common thread is that none of the attackers found a bug in npm's or PyPI's infrastructure — they got a human or a token to hand over legitimate publish access. That's a rational attacker choice: registries authenticate publishers, not code, so a valid credential bypasses every downstream check a consumer might run against package content. Phishing (fake 2FA-reset emails, typosquatted support domains like npmjs.help), stolen CI/CD tokens, and informal maintainer handoffs to unvetted volunteers are all functionally equivalent from the registry's point of view — they all produce a "trusted" publish. This is why maintainer-side controls (mandatory hardware-key 2FA for publish rights, granular tokens scoped to a single package, automation-only publishing from CI rather than developer laptops) matter as much as anything a downstream consumer can do, and why npm has progressively tightened publish requirements for high-download packages in the years following these incidents.
What telemetry signals actually catch these attacks before install?
Across all three incidents, the same categories of signal would have surfaced the compromise before or shortly after publish: a sudden new maintainer or publish-token origin with no prior history on the package; a version bump with no corresponding source commit, or a minified/published bundle that diverges from the tagged GitHub release; install-time or runtime network egress to newly registered domains or unfamiliar endpoints; and — specific to worm behavior — one compromised package's publish event closely followed by publish events on other packages tied to the same maintainer account. None of these require executing the payload to detect; they're observable from package metadata, diffing published artifacts against source, and monitoring egress at install time. That last point matters because, as event-stream showed, source-only code review is provably insufficient — the check has to run against what actually gets installed.
How Safeguard helps
Safeguard's Eagle classification model scores exactly these indicator classes on every npm and PyPI publish — install-script behavior, egress patterns, credential-harvesting file access (~/.npmrc, ~/.aws, ~/.ssh), typosquat similarity, and metadata anomalies like a brand-new package with an aggressive versioning pattern — and re-scores the historical corpus retroactively when Eagle's model improves, so a package reclassified as malicious after the fact still generates a finding. The Package Firewall sits earlier in the chain: it's an install-time proxy fronting npm and pip that evaluates every fetch — including transitive dependencies — before code reaches disk, with allow/warn/block and quarantine modes so a suspicious package can be held for review instead of silently installed. Together they cover both sides of the pattern this post describes: Eagle for the behavioral and metadata signals a compromised publish leaves behind, and the firewall for stopping a bad fetch, including a worm's self-propagated one, before it lands in your tree.