· via dev.to (home feed)
Malware hidden in vite.config.js runs on npm run build after forged merge commit
A dev.to investigation documents how attackers copied a legitimate GitHub merge commit, injected obfuscated malware into vite.config.js, and force-pushed it so the payload runs the moment anyone executes npm run build.

A write-up on dev.to documents a supply-chain attack that requires no malicious package and no suspicious file: the attacker forges a routine GitHub merge commit, injects obfuscated malware into vite.config.js, and force-pushes it, so the payload runs the moment anyone executes npm run build after a git pull.
According to the author, who generalized the findings from a real investigation, several security vendors have tracked related activity as a campaign against the npm and Vite ecosystem under names including ChainVeil, ViteVenom and PolinRider, and GitHub users have independently reported build configs rewritten by force-pushes.
How it spreads
The chain starts when malware on one team member's machine steals Git hosting credentials. A script then force-pushes a poisoned commit to every branch of every repository those credentials can reach. Only one file is modified — a build config — while package. and node_modules stay untouched, so dependency audit tools flag nothing.
Speed matters here. In the observed incident, 14 minutes after a legitimate pull request merge, a forged copy of that merge overwrote the develop branch; about a dozen branches followed within roughly a minute, some two seconds apart. Production and staging branches were rewritten too, so an active CI/CD pipeline could carry the payload all the way to deployment.
Hiding in the diff
The payload is appended to the end of vite.config.js behind a long run of whitespace. In a diff it looks as if only the closing line changed, and the stat summary reports a two-line change — the kind of churn almost nobody opens. ESM configs receive a small shim at the top so require keeps working. Obfuscation runs two layers deep: a character substitution restores a string table, dictionary expansion rebuilds the code, and the result runs through the Function constructor, keeping recognizable keywords out of static searches.
A C2 address on a blockchain
The malware's second stage exists only to find its command-and-control server, and it does so by reading a public blockchain. It connects to a public Ethereum RPC endpoint, walks back through recent blocks, finds a transaction whose sender address contains a specific marker, and assembles the C2 address from the recipient. Three RPC endpoints are tried in order as fallbacks.
The write-up identifies the technique as EtherHiding and notes variants reported on TRON, Aptos and BNB Smart Chain. It is hard to counter: seizing the server achieves little because the attacker can post a new address on-chain; the traffic targets legitimate public RPC infrastructure, so destination blocking is impractical; and the on-chain record cannot be deleted.
Arbitrary code, no trace
Once booted, the resident process can disable itself via a -skipwarn startup flag, guards against running twice, fetches an initialization payload from the C2, and passes it straight to eval. Since require is exposed globally, whatever arrives can read and write files, exfiltrate data or install persistence. Because eval executes in memory, the author notes, nothing lands on disk — afterwards it is essentially impossible to determine what was stolen.
The forged merge commit
The entry technique is the standout. The attacker copied a genuine merge commit wholesale — identical author, author date, parent commits and merge message — rebuilt it with the payload, and force-pushed it over the branch. GitHub's web interface showed one ordinary merge. Three details exposed it: the committer field carried the member's name with a +0100 timezone instead of GitHub <[email protected]>, the PGP signature was absent, and vite.config.js had grown from 963 to 9,510 bytes.
The name on the forged commit belonged to a different, uncompromised member. Author fields are free text; the credential that performed the push is the unfakeable part, and GitHub records it in the repository's event log, reviewable through PushEvent entries via the API.
How to spot one
The checks follow directly from the tells. Merges made through GitHub's button always list GitHub as the committer; a merge commit credited to a person was not merged on the web. Signature status can be audited with git log's %G? format placeholder, where G means a good signature and N means none. A committer timezone the named developer's machine could never produce — +0100 in a +0900 environment — is a further red flag.
Why it matters
This attack bypasses the surfaces most teams monitor. Nothing passes through the package registry, so lockfiles and dependency scanners are blind to it; the infection sits in a config file effectively every JavaScript project has, and the trigger is a command developers and CI servers run constantly. Git's human-readable metadata — author, date, message — is trivially forgeable, leaving signatures, committer identity and the platform's push logs as the only reliable signals. With a C2 channel that survives server seizures by design, repository credentials deserve production-secret handling: branch protection against force-pushes, required signed commits and periodic committer-field checks are cheap and would have caught this.
- #supply-chain
- #security
- #npm
- #git
- #vite
- #malware