Security1 publisher2 min readPublished
PhantomRaven hid its credential stealer in an npm dependency that scanners never fetch
Koi Security counted 126 npm packages and more than 86,000 installs since August 2025, with 80 still live when it published. npm pulled the stealer from the attacker's host at install time. That put it outside the package a scanner reads.
The Watch · Security desk
What happened
- Koi Security counted 126 malicious npm packages with more than 86,000 combined installs, collecting npm tokens, GitHub credentials and CI/CD secrets from developers.
- Eighty further packages went up across September and October 2025 after npm removed the campaign's first 21 in August, and that later batch evaded detection.
- The published packages contain a hello world script, and the payload is declared as an HTTP URL dependency that npm fetches from the attacker's host at install time.
- npmjs.com and security scanners do not follow those URLs, so the registry and dependency analysis tools report the packages as having 0 dependencies.
Compiled by The WatchSomething wrong?How this is made
Why it matters
- exposure The credentials in reach belong to the build system: a runner installing a transitive dependency executes the payload with whatever publishing and repository access the job was granted.
- constraint A clean dependency tree and a pinned lockfile stop answering the question being asked, because the URL's contents can change between installs while the manifest stays identical.
- capability Server-side control of the payload gives the operator a per-request choice of what to deliver, including a clean period long enough to pass scans; Koi presents that as theoretical.
- decision With 80 packages live at publication, teams are choosing between takedown-and-rotate and waiting, and there is no version bump to apply.
Koi Security says the HTTP dependency is re-fetched from the attacker's host on every `npm install`, uncached, unversioned and unrecorded in any lockfile [10]. So the contents behind that URL can change between installs while the manifest stays identical, and last week's build and today's can run different code.
Behaviour is what caught this one. Koi's Wings engine flagged packages making outbound requests to one domain during installation, in October 2025 [3]. Static review of the published package finds a hello world script and nothing else [7]. npmjs.com leaves HTTP dependency specifiers unresolved and scanners walk past them, so the registry page reports 0 Dependencies [9].
More than 86,000 installs across 126 packages averages about 683 per package [16]. The published timeline dates 101 of them: 21 removed by npm in August 2025, and 80 uploaded across September and October that got through [4][5][17]. That leaves 25 undated. Eighty were still live when Koi published, 63 percent of the set [6][18].
What Koi observed is install-time traffic to a single domain and credential collection; the company wrote that the packages were "actively stealing npm tokens, GitHub credentials, and CI/CD secrets from developers worldwide" [3][15]. Koi offers the selective-serving scenario as theory: check the requesting IP, return benign code to researchers on VPNs and the payload to corporate networks, or serve clean code for weeks to pass scans and flip later [11].
npm runs the code on its own. The fetched package declares a `preinstall` hook that runs `node index.js`, and npm executes it automatically, unprompted and with no user interaction [12]. It works the same at depth: a package three levels down the tree still runs its preinstall on the machine doing the install [13].
Two controls sit on that chain. Egress filtering on build runners breaks the fetch from `packages.storeartifact.com` [8][10]. Refusing to run lifecycle scripts at install breaks the execution step [12]. Dependency-tree analysis reads an empty tree and sits outside the chain [9].
The operator was careless about identity. Koi lists sequential free-mail accounts from [email protected] through [email protected], and registry usernames including npmhell and npmpackagejpd [14]. The delivery technique kept working through 80 further uploads after npm took down the first 21 [4][5].
What to watch
- Whether npm changes how it treats HTTP dependency specifiers, or begins resolving them so scanners can see them.
- Whether the 80 live packages come down and whether packages.storeartifact.com stays reachable.
- Whether a second operator reuses remote dynamic dependencies from a different host.