Hacked npm package shipped malware with valid provenance

Published

On 9 September someone spent 105 minutes inside the account of a working npm maintainer and used it to ship a release that the registry's own anti-tampering machinery then certified as genuine.

That is the uncomfortable part. Nothing broke.

The package was @dforge-core/dforge-mcp, a small but real project. Researchers at CloudSEK, in a report written by Vikas Kundu, reconstructed what the intruder did with the time. They added a remote code loader. They changed three lines so that any push to the main branch would start the release workflow, rewrote that workflow fourteen minutes later so it could publish without a human approving it, and pushed the result out as version 0.2.21. The maintainer caught it, reverted everything, and published a clean 0.2.22. CloudSEK has published the original report, which names the loader family GHAPPIER and traces it across at least 65 public repositories, 73 infected files and 22 accounts.

Thirty-five minutes, recorded to the second

CloudSEK puts the exposure window at 35 minutes and 38 seconds. npm's own registry record backs that figure exactly. The registry still timestamps 0.2.21 at 17:19:50 UTC on 9 September and 0.2.22 at 17:55:28 the same afternoon, a gap of 35 minutes and 38 seconds, matching the researchers' number to the second.

That record also shows how the cleanup was done. Neither 0.2.21 nor the 0.2.20 that preceded it is installable today. Both have been removed from the package's list of published versions, while their publication timestamps survive in the registry's history, which is the ordinary signature of an unpublish rather than a patch. The project has since moved on, and 0.2.23 became the current release on 15 September.

A signature that told the truth

The malicious release carried valid npm provenance. It was built by GitHub Actions through OIDC trusted publishing, and its attestation still sits in Sigstore's append-only transparency log, naming the attacker's own commit.

Nothing was forged and nothing malfunctioned. Provenance exists to answer one narrow question, and it answered that question honestly: it records where an artefact was built, not whether the source code that went into it was honest. The attestation is an accurate record of a dishonest input, which is a very different thing from a broken control, and a much harder problem to fix.

It also explains why no npm credential had to be stolen. Under trusted publishing the registry trusts the repository's continuous integration identity rather than a person's token, so the ability to push to the main branch was, in practice, the ability to publish to the registry. That is the same property that made an earlier npm worm dangerous when it was found to be set up to sign its own malware with real provenance. The supply chain defence that developers have been told to look for was present, correct, and beside the point.

One line in a ninety-nine kilobyte file

The payload itself was a single line, sitting at line 3320 of a 99 KB file. It opens a four-stage chain, and CloudSEK reports that the final implant deletes itself from disk the moment it runs.

That detail matters more than it first sounds. An organisation that installed the bad release, then swept its machines for a malicious file, would find nothing, and could reasonably conclude it had escaped. The evidence that it did not would be somewhere else entirely. CloudSEK also reports that when it tested the chain five days after the version was withdrawn, every stage still answered, so pulling the package removed the cheapest component of the operation and left the rest of it reachable.

A command server with no address

Late in the investigation the researchers found a second payload family in another victim's repository, and it is the one that ties this activity to something larger. Its signature is a payload appended silently to the end of a genuine project configuration file, which CloudSEK identifies as the calling card of PolinRider, an operation that the OpenSourceMalware project has tracked across thousands of repositories since March 2026.

This family carries no command-and-control address at all. It reads one off the Ethereum blockchain, from a wallet already published as an indicator for that campaign, and CloudSEK reports that an exact match was still beaconing while the report was being written. The configuration is written into the twenty bytes of a recipient address on an empty transaction that costs roughly twenty cents to send. There is no domain to suspend, no host to seize and no account to disable, which is the same takedown-resistant trick seen in the botnets that have begun hiding their servers on public blockchains. Twenty cents buys a channel that the usual remedies cannot touch.

What the link does not prove

The attribution here deserves to be stated carefully, because the researchers state it carefully themselves. Campaign membership is CloudSEK's own finding, established from the sample it analysed. The further step, that PolinRider is a North Korean operation, belongs to the researchers who documented the related NullReceiver activity; CloudSEK cites that assessment rather than asserting it, and says plainly that the one independent check it was able to run did not confirm it. Readers should treat the North Korean link as a cited third-party judgment, not as a settled fact, and certainly not as something this report established.

How the intruder got push access in the first place is the one thing CloudSEK says it cannot answer definitively. Its suspicion is that a developer was running a malicious extension or package. The campaign's documented method, harvesting a developer's cached git credentials from an already-infected machine and then pushing with them, is the most plausible account available, and it would explain why the same borrowed name turns up across dozens of unrelated repositories.

Pin to 0.2.22, then hunt the leftovers

CloudSEK's immediate guidance is to pin the package at 0.2.22, block the socket endpoint and the two delivery hostnames it names, and sweep for the artefacts the chain leaves behind rather than for the implant, which removes itself at startup. Anyone acting on that should note that the registry has since advanced to 0.2.23, so a pin written today should be a deliberate choice rather than a copied version string. The endpoint and hostname indicators, along with the malware analysis and the full attack timeline, sit in the downloadable report rather than in the public write-up, so defenders who want the blocklist will need to pull that.

The exposed population is small, and nothing in the research evidences a successful compromise at any organisation. The more useful finding is about what happened afterwards. A version was withdrawn, and that was treated as the end of it: there is no advisory in OSV, none in GitHub's database, and none from the maintainer. Anyone still holding 0.2.21 has been told nothing at all, and the self-deleting implant means they will not find it by looking for a file. A pulled version closes the front door and leaves everyone who already walked through it uncounted.

This briefing is provided by IntelFusions for informational and defensive purposes only. It is based on sources assessed to be reliable at the time of writing, and analytic judgments carry the confidence levels indicated. Indicators of compromise are defanged; re-arm them only in controlled environments. IntelFusions is not affiliated with the organizations named and makes no warranty as to completeness or accuracy.

Read the full analysis on IntelFusions