WeaselBiscuit ran on import, not install, so ignore-scripts would not have stopped any of the 17 npm packages
A suspected DPRK stealer shipped in 17 npm package names over five days, and none of them used an npm lifecycle script. It swept every Chrome extension storage directory it could read, with no target list at all.
By Parminder Kumar Sharma · · 18 min read

The control everyone recommends was on the wrong gate
The standard supply-chain advice after every npm incident is to stop packages executing code at install time: run npm ci --ignore-scripts, set ignore-scripts=true in .npmrc, disable lifecycle hooks in CI. It is good advice and it is worth doing. It would not have stopped a single one of these 17 packages.
The research states plainly that the package "has no npm lifecycle hook, so execution requires import or manual start". Every OSV record for the cluster describes the same trigger: "On require/import, index.js unconditionally calls initialize()". The malicious behaviour begins when application code loads the module, not when the package manager unpacks it. Installing it and never importing it does nothing. Importing it once, in a test run, in a linter, in a developer's local server, starts a detached background Node process that outlives the parent.
This is the friendly-name fallacy in its purest form. "We block install scripts" reads like a statement about supply-chain risk. It is a statement about one specific execution trigger. The second thing worth noticing on that diagram is that the malicious code never shipped in the package at all. The published tarball contains a loader that fetches JSON from api.npoint.io, a free anonymous JSON host, decodes a Base64 code field and runs it through new Function in memory. Nothing hits disk. A scanner that reads the tarball sees a small fetch-and-evaluate loader; the behaviour that matters is whatever the operator put in the bin that morning, and bins are mutable.
Three numbers for one cluster, and none of them is the total
The research that named WeaselBiscuit says it found 11 npm packages. The same team's companion repository, published the day before, says "These are the 16 packages we've found so far" and lists 16. The Hacker News, reporting the research on 18 September, says 13 and prints a list of 13. Those three lists are not subsets of one another. Collect every distinct package name that appears anywhere across the report, its indicator table, the repository and the news coverage, and the total is 17.
The arithmetic is simple enough to do by hand. The report's first-seen table has 16 rows. Its indicator-of-compromise table names 13 npm packages, but one of those, swnwall, never appears in the 16-row table at all. The Hacker News list of 13 drops swnwall and adds process-mite, and leaves out laycot, @railone/image-utils and @vibecheck-polid/process-runtime-utils entirely. Sixteen plus swnwall is 17.
Package counts across the primary research, its repository and the news coverage. Counted from the published lists on 18 September 2026.
| Source and what it says | Names it lists | Names it omits |
|---|---|---|
| OpenSourceMalware blog prose: "11 npm packages" | No list attached to the number | Not applicable |
| OpenSourceMalware blog, first-seen table | 16 | swnwall |
| OpenSourceMalware blog, indicator table | 13 | laycot, process-mite, and both other scoped names |
| OpenSourceMalware repository README: "16 packages" | 16 | swnwall |
| The Hacker News: "13 npm packages" | 13 | swnwall, laycot, and both other scoped names |
| Union of every published name | 17 | None |
This is not a gotcha. Research written over five days while the operator kept publishing will drift, and the vendor is explicit elsewhere about what it has and has not proved. But the number is the first thing a security lead reads, and it is the number that goes into a ticket. If your hunt query was built from the headline, it is missing four names.
What the count does not establish
Seventeen names is not 17 compromises, 17 victims or even 17 distinct payloads. The research recovered and hash-matched exactly one second stage, SHA-256 7b15605f...5159, and confirmed five stage identifiers: 10, 12, 79, 95 and 99. Everything else in the cluster is linked by shared indicators, chiefly the same loader template, the same dead-drop service and the same command-and-control address, not by a recovered payload per package.
The count also says nothing about reach. Downloads for the seven unscoped packages, taken from npm's own download API for 10 to 18 September, total 859 across five packages. Two of the seven, including process-tailwind, the package whose infection chain the research actually confirmed end to end, record zero downloads for the whole window. A package with a confirmed live payload and a zero in the counter is a useful reminder of what those counters are: a registry traffic metric that is dominated by mirrors, proxies and scanners, and that does not resolve to installs on developer machines.
Downloads and lifetime for the seven unscoped packages. Computed from registry documents and the npm downloads API, retrieved 18 September 2026.
| Package and version | Downloads, 10 to 18 Sep | Time on the registry |
|---|---|---|
| id79-client 1.1.79 | 191 | 51 h 33 m |
| process-tailwind 1.1.99 | 0 | 15 h 59 m |
| process-lhpm 1.1.79 | 0 | 15 h 35 m |
| engin1 1.3.99 | 196 | 12 h 52 m |
| swnwall 1.2.10 | 199 | 11 h 29 m |
| process-mite 1.1.79 | 182 | 7 h 51 m |
| laycot 1.3.10 | 91 | 2 h 46 m |
How they got onto npm: three fresh accounts, not a hijacked maintainer
The primary research does not say how the packages were published. The OSV malware records for the same packages, contributed by Amazon Inspector, do. Nine of the names, all eight under the @biz44 scope plus the unscoped id79-client, are described as "part of a malicious npm campaign published by the biz44 account". laycot is attributed to a laycot account and process-mite to a max233 account. The remaining records do not name an account.
The registry documents settle the rest. Every one of the seven unscoped packages has exactly one published version in its time object, and that version is the malicious one. There is no prior release, no legitimate history, no other maintainer. These are not hijacked packages and they are not typosquats of anything popular: process-tailwind is not a plausible misspelling of tailwindcss, and engin1, swnwall and laycot are not misspellings of anything. They are brand-new names published by brand-new accounts, dressed with a README describing an API, getRuntimeInfo or getProcessInfo, that the shipped code does not export.
Publication route. Drawn from OSV malware records MAL-2026-16165 to MAL-2026-16261 and from npm registry documents, 18 September 2026.
| Question | On the record | Not stated |
|---|---|---|
| Who published | Accounts named biz44, laycot and max233 | Whether any account was itself stolen |
| Prior versions | One version per unscoped package, the malicious one | Anything about the scoped packages' download history |
| Name strategy | New names with invented utility READMEs | Any evidence of typosquatting a real package |
| How a victim arrived | Import by a parent application | How anyone was persuaded to add the dependency |
That last row is the largest gap in the public record. The research is explicit that the package has no npm lifecycle hook, so "execution requires import or manual start". Something had to tell a developer to add @biz44/runtime-utils to a project and then import it. The Contagious Interview pattern that WeaselBiscuit resembles does that with fake job interviews and take-home coding exercises. Nothing in this research demonstrates that route for this cluster, and the vendor does not claim it.
The takedown clock, and a status column that was already wrong
All 17 names are gone from the public registry. The ten scoped names return HTTP 404. The seven unscoped ones still have a registry document, and each carries an explicit unpublished record with a timestamp and the version that was removed. That lets the removal window be computed rather than estimated.
The median time on the registry was 12 hours 52 minutes; the mean was 16 hours 52 minutes. The slowest was id79-client, the first name published, at 51 hours 33 minutes. The fastest was laycot, the last, at 2 hours 46 minutes. The trend across the week runs one way: the longer the campaign ran, the faster each new name came down.
Two rows in the same table also carry timestamps that do not match the registry. The report gives laycot a first-seen of 2026-09-16T13:51:46Z; the registry created it at 2026-09-16T23:57:52Z. The report gives process-mite a first-seen of 2026-09-16T23:57:52Z, which is laycot's actual creation time; the registry created process-mite at 2026-09-16T08:40:49Z. The rows appear to have slipped. This matters only if you are using first-seen to bound an exposure window, which is exactly what an incident responder does.
Removed is not the same as taken down
An unpublished record in a registry document says a version was withdrawn. It does not say who withdrew it. npm's own policy allows a publisher to unpublish a newly created package "anytime within the first 72 hours after publishing" as long as nothing depends on it. Every one of these seven was removed inside that window, the longest at just under 52 hours. On the public record, an operator tidying up after itself and npm's security team removing malware are indistinguishable.
What "Chrome extension storage" means, and which extensions were targeted
The honest answer to the second half of that heading is: none of them. This is where the coverage and the reality diverge most sharply, and it is the single most useful correction in this story.
BeaverTail and OtterCookie carry a hardcoded list of cryptocurrency wallet extension IDs and go after those directories specifically. WeaselBiscuit does not. The research is unambiguous: there is "no hardcoded wallet extension ID list", and the implant uploads "every readable, nonempty file under Chrome profiles' Local Extension Settings directories on Windows, macOS, and Linux". There is no target list because there is no targeting. It takes the directory.
Local Extension Settings sits inside the Chrome profile, under %LOCALAPPDATA%\Google\Chrome\User Data on Windows, ~/Library/Application Support/Google/Chrome on macOS and ~/.config/google-chrome on Linux. It holds one LevelDB store per extension: the persistent half of the chrome.storage API, written by whatever extensions that profile has installed. Google's own documentation draws the line clearly. Items in storage.session "are stored in-memory and will not be persisted to disk", and the reference tells developers that "If you're working with sensitive user data, instead use storage.session". Everything an extension put in the persistent areas instead is sitting in those directories.
Extension targeting compared. From the OpenSourceMalware capability comparison table, 17 September 2026.
| Behaviour | BeaverTail and OtterCookie | WeaselBiscuit |
|---|---|---|
| Hardcoded wallet extension ID list | Yes | No |
| Chrome extension storage theft | Yes | Yes, whole directory |
| Browser credential database decryption | Yes, or partial | No |
| Seed phrase or wallet address regex sweep | Yes, or variant dependent | No |
| Other file exfiltration | Yes | No, extension storage only |
The practical consequence is that "were password managers or wallets affected?" is the wrong question. The right question is: what have the extensions in your developers' work profiles written to disk? A crude sweep reaches an authenticator extension's state, an internal browser tool's cached configuration, a session-management extension's tokens, an AI coding assistant extension's workspace settings, as readily as it reaches a wallet. The research names MetaMask once, as an example of what legitimately lives in such a store, not as a target.
An infected laptop is not a compromised organisation
The gap between those two things is where most of the panic in supply-chain reporting lives, and this cluster sits unusually clearly on one side of it. WeaselBiscuit has no remote shell, no arbitrary command execution, no second-stage downloader, no screenshot module and no persistence beyond a detached Node process and a .pid file. Its control plane is six Express routes polled over plain HTTP every five seconds. In the recorded sessions the two collection switches returned isMonitoring: false, and no command, redirect or next-stage URL was ever observed in a response.
That is a collection tool, not a foothold. Compare it with the Shai-Hulud npm worm activity this site covered on 16 September, where the payload harvested GitHub OAuth tokens and used them to spread across roughly 100 internal repositories. The difference is not sophistication. It is what the implant was built to take, and whether what it takes can be replayed against your systems.
What the recovered implant could and could not reach, on the research's own account.
| Asset | Reachable by this implant | Not reachable by this implant |
|---|---|---|
| Chrome extension storage on disk | Whole directory, all profiles | Anything the extension kept only in session storage |
| Clipboard and Windows keystrokes | Yes, when the operator enables it | macOS and Linux keystrokes |
| Saved browser passwords | No decryptor in the recovered code | Not attempted |
| Source files, keystores, SSH keys, npm tokens | No file exfiltration beyond extension storage | Not attempted |
| Interactive access to the host | No remote shell, no command execution | Not present in the recovered client |
Two caveats that the research states and this briefing repeats. First, the second stage arrives from a mutable third-party bin, so what was recovered is what that bin served on the day it was pulled, and the operator can change it without republishing anything. Second, the attribution is a hypothesis. The vendor rates DPRK attribution "low to moderate" and says it has not recovered operator infrastructure, victimology, campaign metadata or signing material sufficient to conclude it. Treat the North Korea framing in the headlines as an unproven lead.
The version number is the campaign tag
One pattern is worth extracting because it is usable and does not appear in the research as such. The operator tags each build with a numeric identifier, 10, 12, 44, 79, 95 or 99. The research maps five of those identifiers to first-stage dead-drop URLs. Cross-reference the OSV records, which name the dead-drop URL each package fetches, against the version numbers in the registry, and the patch component of the version is the same tag.
Package version against the campaign identifier of the dead-drop it fetches. Version numbers from npm registry and OSV records; dead-drop to identifier mapping from the OpenSourceMalware report and its YARA rules.
| Package and version | Identifier of the dead-drop it fetches | Patch number matches |
|---|---|---|
| process-tailwind 1.1.99 | 99 | Yes |
| engin1 1.3.99 | 99 | Yes |
| swnwall 1.2.10 | 10 | Yes |
| laycot 1.3.10 | 10 | Yes |
| @railone/image-utils 1.1.10 | 10 | Yes |
| process-lhpm 1.1.79 | 79 | Yes |
| process-mite 1.1.79 | 79 | Yes |
| @biz44/id44-client 1.1.44 | 44 | Yes |
| id79-client 1.1.79 | 44 | No, and the research flags this one as unproven |
Eight of nine. The single exception is id79-client, which calls itself an "ID-79 Client Module" and carries version 1.1.79 but hard-codes the dead-drop that the vendor's own YARA rule labels ID 44. That URL returned HTTP 404 when the researchers retrieved it, and the report explicitly says the delivery link for that package "is not proven". The rule survives its own exception.
This is worth having because it is a hunting shortcut that does not depend on the vendor's tables staying current. A package you have never heard of, from an account with no history, whose only version ends in a suspiciously round patch number and whose index.js calls something on import, is worth opening. The pattern also makes the next batch of names easier to spot than the names themselves, which is the whole point, because the names change every day and the build convention did not.
A note on the research and who published it
OpenSourceMalware is a commercial platform that sells npm package scanning, and its first recommended investigation priority is to sign up for its own free tier and scan your estate with it. That is normal, and the research itself is unusually careful in the ways that matter: it publishes a confidence table that rates its own headline attribution "low to moderate", it separates confirmed payloads from indicator-linked clones, and it ships three YARA rules and a deobfuscated second stage so that other people can check the work. The weaknesses found above are drift and copy errors in a fast-moving write-up, not overreach in the analysis. The method is sound; use the tool or do not, but the indicator set is worth taking.
What a UK engineering team should do this week
Take this with you
In this order
- Search every lockfile in every repository, not just the ones you think are active, for the 17 package names and for the @biz44, @railone and @vibecheck-polid scopes. Lockfiles are the record of what was resolved, and they survive the packages being unpublished.
- Search your npm proxy or artifact repository logs for the same names over 12 to 18 September. If you run a pull-through cache, that log is your best evidence of whether anything was actually fetched, and it is better evidence than the registry download counters.
- Search outbound network logs and DNS for 103.170.217.184 on port 8787, for api.npoint.io, and for the six dead-drop paths listed in the research. A developer machine talking to a free JSON hosting service is not by itself suspicious; one that then polls a bare IP address every five seconds is.
- On any host that matches, look for stray .pid files inside node_modules, detached node child processes with no parent application, and on Windows the temp path kb-monitor holding keyboard-monitor PowerShell files.
- If a host matches, rotate what the implant could actually reach, in this order: any credential a developer copied to the clipboard in the window, any token or secret held by a browser extension in that Chrome profile, and any wallet or signing state in that profile. Do not start with the crown jewels; start with extension storage, because that is what it took.
- Do not assume your source code, SSH keys or npm publish tokens were taken. The recovered implant has no general file exfiltration. Say so explicitly in your incident notes, because the alternative is an unbounded rotation exercise that costs a week and finds nothing.
- Keep ignore-scripts on in CI, and stop describing it as supply-chain protection. Write down what it does cover and what it does not, and treat first import of a new dependency as a distinct event worth logging.
- Add the three published YARA rules to whatever scans your build artefacts and developer endpoints. The first-stage rule is deliberately broad and will flag any package that fetches from a JSON host and evaluates a Base64 field, which is a lead rather than a verdict, and is exactly the right sensitivity for a build pipeline.
- Decide who in your organisation owns the question of which browser extensions are permitted on engineering laptops, and what they are allowed to persist. If the answer is nobody, that is the finding, not the malware.
# Lockfile sweep across a checkout of every repository
grep -rlE '(@biz44/|@railone/image-utils|@vibecheck-polid/|process-tailwind|process-lhpm|process-mite|id79-client|engin1|swnwall|laycot)' \
--include=package-lock.json --include=yarn.lock --include=pnpm-lock.yaml \
--include=npm-shrinkwrap.json --include=package.json .
# Stray loader PID markers left inside installed packages
find . -path '*/node_modules/*' -name '.pid' -print
# Detached node processes with no obvious parent application
ps -eo pid,ppid,etime,command | grep -i 'node .*loader.js' | grep -v grep
The question that exposes the gap
The reason this cluster is worth a long look is not the malware, which is the least capable thing in its family. It is that a stealer with no install hook, no persistence, no remote shell and no file exfiltration still walked off with the entire on-disk state of every browser extension on any machine that imported it, and that the control most teams would name if asked how they defend against npm supply-chain attacks sits at a gate it never went through.
So the question to put to your own team this week is not whether you have any of these 17 packages. You almost certainly do not; they were live for a median of under 13 hours each. The question is: if a dependency ran code the first time one of our developers imported it, rather than the moment it was installed, what in our tooling would have noticed, and what would it have been able to reach? If the honest answer to the first half is nothing, the second half is the only thing standing between a developer's laptop and the rest of the estate.
Key facts
Sources
- PrimaryPrimary research, 17 September 2026: the WeaselBiscuit write-up, package table, infection chain, capability comparison, confidence table and indicators of compromiseOpenSourceMalwareaccessed 2026-09-18
- PrimaryThe companion repository README and YARA rules: the 16 package list, the resolver-to-identifier mapping including the ID 44 label, and the no-lifecycle-hook statementOpenSourceMalwareaccessed 2026-09-18
- Primarynpm registry document used to verify publication and unpublication timestamps and the single published version for each unscoped packagenpm, Inc.accessed 2026-09-18
- Primarynpm download API used to compute per-package download counts for the campaign windownpm, Inc.accessed 2026-09-18
- PrimaryOSV malware records MAL-2026-16165 to MAL-2026-16261, sourced from Amazon Inspector, used to confirm publisher accounts, affected versions and per-package resolver URLsOpen Source Vulnerabilities (OSV)accessed 2026-09-18
- Primarynpm unpublish policy, used for the 72 hour self-unpublish window and the conditions on older packagesnpm, Inc.accessed 2026-09-18
- PrimaryChrome extensions storage API reference, used for the storage.local and storage.session distinction and the sensitive-data guidanceGoogleaccessed 2026-09-18
- PrimaryChromium documentation for the default user data directory paths on Windows, macOS and LinuxThe Chromium Projectaccessed 2026-09-18
- PrimaryTalos research on BeaverTail and OtterCookie converging, cited by the primary research as the lineage argumentCisco Talosaccessed 2026-09-18
- Reported byNews coverage, 18 September 2026, which reported 13 packages and supplied the Jenn Gile statementThe Hacker Newsaccessed 2026-09-18


