It asked to be run
Sonatype is tracking 846 malicious npm packages published from hundreds of disposable accounts under AI-generated names. None of them carry an install script. The README tells you to load the package with require(), and the import runs the loader. Defenses that recognize a bad package by name, author, or declared hook are now racing a generator. Bromure Agentic Coding blocks on the one property a generator cannot forge: how old the package is.
The packages carry no install hook, so npm has nothing to catch. The README
tells you to load them with require(), and the import runs the loader.
More and more, the reader following that instruction is a coding agent.
Sonatype Research Labs disclosed a campaign it calls Flooding Dropper on August 5, counting 846 malicious npm components and still climbing. Paul McCarty at OpenSourceMalware tracks the payload as WEL1DROPPER. The broader coverage landed on August 7.
Two details set it apart from the npm supply-chain stories of the past month.
The first is how the packages get their names. The attacker generates them,
running slopsquatting as a production line. Recurring stems like bigops and
bnpl recombine with other words into names such as bigops-api and
dolyame-boxy-desktop-bnpl-card-gallery, many sharing a 35.x.y version
pattern. The publishing spreads across hundreds of disposable accounts
carrying a handful of packages each, so there is no prolific author to ban.
No install script
The packages declare no lifecycle hooks at all, so npm install runs nothing.
McCarty's write-up states it outright: "The NPM packages do not use a
preinstall or postinstall script. It doesn't need one."
They ship a bundled _helpers.js that runs when the module is imported,
alongside a README telling the reader to import it. Load the package with
require(), says the documentation, and the documentation is correct. The
first stage fingerprints the operating system and processor architecture,
pulls a native second stage over HTTPS from one of three Cloudflare Workers
hosts, and falls back to reassembling the binary out of base64 chunks in DNS
TXT records under wel1.ru when the direct fetch fails.
The second stage varies by platform. Linux gets a UPX-packed ELF that deploys a Sliver C2 implant. macOS gets anti-debugging checks, a probe for VMware, a beacon binary, and LaunchAgent persistence. Windows gets ETW and AMSI patching, sandbox detection, reflective in-memory execution, and persistence through a Registry Run key and a scheduled task. McCarty and Sonatype both read the campaign as an evolution of "Moika," the dependency-confusion run from April that pushed 250-odd packages built around environment theft.
The attacker moved the trigger off the package manager and onto the reader.
Automation beats the deny list
Sonatype's own summary is the sharpest line in the coverage:
Read that as a statement about defenses. Every layer that protects you by recognizing the bad package, whether by name, by publishing account, or by declared hook, now competes with a generator. Those layers still catch what they have already seen. They are graded on recognizing a specific string, and the supply of specific strings costs a script run.
The agent angle sharpens it. Slopsquatting pays in the first place because language models invent package names that sound right and do not exist, and an attacker who registers those names collects install traffic for free. Add the missing install hook and you get a package whose trigger is a written instruction in a README. A coding agent is the reader that will hallucinate the name, find a package sitting under it, read the documentation, and do what the documentation says. The campaign is shaped for the agent on both ends.
Pick a property the generator cannot forge.
Age is not a name
Every package in this campaign shares one property that no amount of automation changes: it is new. A name a model invented last Tuesday cannot have a package under it from 2019. Hundreds of disposable accounts publish hundreds of brand-new versions, because a disposable account has no history to publish from. The model depends on the package being there by the time someone reaches for it, so the attacker creates it days ahead at most.
Bromure Agentic Coding runs the agent inside a disposable Linux VM on Apple Silicon and routes every byte of its network traffic through a proxy on the host, outside the box the agent runs in. Package fetches for npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go modules and Packagist all cross that proxy, and the first policy they meet is a clock.
The age gate, on by default
Bromure refuses packages younger than a minimum age, set to two days out
of the box. Floating references such as latest or a semver range resolve
to the newest version older than the cutoff, so ordinary installs keep
working and land on yesterday's release. A pinned reference to a too-fresh
version gets a 451 with a clear Bromure error naming what happened. Eight
hundred packages cost as much to stop as eight, because Bromure recognizes
none of them.
It runs before the argument starts
The clever part of this campaign, dropping the lifecycle hook and moving
execution to require(), describes what happens after the tarball reaches
disk. The age gate sits at the fetch, in the host proxy, upstream of all
of it. Whether the payload would have run at install time or import time
is a question about a package that never arrived. The in-VM .npmrc and
pip.conf can tighten this policy and cannot loosen it.
Reputation, behind the clock
With a socket.dev key, Bromure vets each package against socket.dev's database before letting the fetch through and blocks the ones flagged as compromised, malware, or typosquats. OSV lookups block packages carrying known vulnerabilities at or above a severity you choose. Delpi re-routes every npm request to a filtering registry outright. These layers recognize things, which makes them useful and puts them second.
The README is scored as what it is
"Load this with require()" is an instruction written by a stranger,
arriving in text the agent reads. Bromure scores the tool output the agent
consumes, including file contents, web pages and fetched documentation,
with a local prompt-injection model. It scores the instruction files the
agent treats as authority, CLAUDE.md and AGENTS.md and their variants,
with a dedicated classifier plus a scanner for invisible-Unicode payloads.
Nothing leaves the Mac to do it.
The payload profiles the wrong machine
Suppose the fetch goes through anyway, because the package aged past the cutoff or an allowlist entry exempts it. Follow WEL1DROPPER through a Bromure profile and watch it do its job against a machine you were going to throw away.
The loader's first move is to fingerprint the host. In a Bromure profile the
host it fingerprints is an Ubuntu VM on NAT, one hypervisor away from macOS,
so it takes the Linux branch and plants a Sliver implant there. It never
requests the macOS branch: the anti-debugging, the VMware probe, the
beacon_mac.bin download, the LaunchAgent that would survive a reboot, the
interest in Russian payment domains. All of it is code for a machine the
dropper never saw. Erase home discards the layer it landed in and resets
the home directory to its post-clone state.
Then the infostealer stage goes looking for what its Moika ancestor went
looking for: the environment. In a Bromure profile the environment holds
brm_… placeholders. None of the real Anthropic, OpenAI and xAI keys, the
GitHub and GitLab tokens, the AWS material or the registry credentials sit in
the VM's memory or its dotfiles. Bromure keeps them on the host and
substitutes them into the request at the proxy, on the wire, for the one
destination host each belongs to. The theft runs to completion and ships
strings that resolve on one Mac, which is not the machine that ran the code.
The hunt they recommend, already running
McCarty closes the WEL1DROPPER write-up with defensive advice: hunt for DNS
TXT queries to wel1.ru, review proxy logs for traffic to the Cloudflare
Workers hosts, and look for stray temporary files and LaunchAgents.
Read that second item again. Your laptop does not keep a proxy log. A Bromure
profile keeps one by construction: every request the VM makes crosses the host
proxy, and the Security Log and Trace Inspector record the host, status,
latency and swap report for each. Three unfamiliar *.workers.dev
destinations appearing during a build are three rows on a list you can read,
on the machine the build ran on, without deploying anything to get them.
Registries will keep taking these packages down, and takedowns work: 846 of them are gone or going. But the campaign is already past a thousand, published from accounts that cost nothing to make under names that cost nothing to invent, and the next wave is a script run away. Recognition is the wrong contest to enter.
A two-day-old package earns no deep trust that a two-hour-old one lacks. It sits outside the window this economy operates in, and you can move your dependency graph behind that window with a setting that is already on.
Install Bromure Agentic Coding, and let the fetch meet a clock before it meets your machine.