Black Shard

Insights28 January 2026

When the package name is the attack

Typosquats, dependency confusion and AI-invented package names all turn one wrong install command into code execution on a developer laptop or CI runner. What each attack looks like, and the checks that catch it before the install runs.

Two nearly identical brass keys side by side on dark slate, one with a subtly wrong cut, lit cold cyan

The name is the only thing the install checks

npm install and pip install take a name and resolve it against a public registry that anyone can publish to. Registration is free, takes minutes, and involves no review of what the code does. If the name exists, the download begins, and in most default setups code from that package executes on your machine before you have read a line of it. That makes the name itself an attack surface, and three attack families work it. Typosquatting registers a near miss of a popular name and waits. Dependency confusion publishes your internal package names to the public registry and lets the resolver prefer them. And the newest family registers names that never existed at all, invented by coding assistants and harvested by attackers before anyone checks.

None of these attacks needs a vulnerability in a real package or a compromised maintainer account. The attacker publishes something new under a name someone else will eventually type, copy or accept. The machine that pays for the mistake is rarely a production server. It is a developer laptop with cached credentials, or a CI runner holding deploy tokens and cloud access, because those are the machines that run installs all day.

Typosquatting waits for one slip

The classic version is a name one edit away from something popular. In 2017 npm removed a batch of packages including crossenv, a typosquat of the widely used cross-env, whose install script harvested environment variables and sent them to an attacker's server. PyPI has removed the same pattern repeatedly, including typosquats of colorama and requests. Variants add plausible prefixes and suffixes rather than misspelling: python3- in front of a real name, or -js or -sdk after one. The payload almost always sits in the install hook, so the victim never has to import anything. Running the install is enough.

The economics favour the attacker. Publishing costs nothing, the fake can sit unnoticed for months, and a handful of mistyped installs across the whole ecosystem still lands on machines full of credentials. The defence sits at the moment of typing. Copy install commands from the project's own repository or documentation rather than from memory, a forum answer or a chat window. Before the first install of anything new, open its registry page and look at the publish date, the release history, the download counts and whether the linked repository actually contains the code. A package with a famous-looking name, a fresh publish date and almost no downloads has earned a minute of suspicion.

Dependency confusion wins on version number

Most organisations that build internal packages give them internal names and never register those names publicly. Dependency confusion exploits the gap. An attacker who learns an internal name, often from a leaked lockfile, a public build log or a package.json committed to an open repository, publishes a package under that exact name on the public registry with an absurdly high version number. A resolver configured to consult both the private and the public registry sees two candidates and prefers the higher version. Researcher Alex Birsan demonstrated this in 2021 by publishing harmless packages under the internal names of major technology companies and watching his code execute inside their build pipelines.

The Python ecosystem has a specific trap here. pip's extra-index-url option merges indexes with no order of preference and takes the best matching version from any of them, which is exactly the behaviour this attack needs. The fix is structural rather than behavioural. On npm, put internal packages in a scope such as @yourco, own that scope on the public registry so nobody else can publish into it, and pin the scope to your private registry in .npmrc. In Python, run installs against a single index, a private one that proxies the public packages you approve, so an internal name can never fall through to the public registry. Where scoping is unavailable, claim your internal names publicly as empty placeholders, and alert if anything ever publishes over them.

Hallucinated names are already being harvested

Coding assistants invent package names. Ask for code that talks to a particular API and the model may confidently import a library that has never existed, complete with a plausible name and a plausible function signature. Studies of code-generating models have found these invented names are common and, more importantly, repeatable: the same fictional package names come back across different sessions and different users. That repeatability is what makes them harvestable. An attacker can run the same prompts, collect the names the models keep inventing, and register them on the public registry loaded with a payload before any real developer goes looking. The pattern has picked up the name slopsquatting.

This is not hypothetical. In 2023 a security researcher noticed assistants repeatedly recommending a PyPI package called huggingface-cli, which did not exist, and registered it as an empty placeholder. It went on to record thousands of downloads, and installation instructions for it appeared in a real project published by a large technology company. Had the placeholder carried a payload, every one of those installs would have executed it.

The working rule is to treat an assistant's install suggestion as a claim that needs verification, exactly like a code suggestion. Confirm the package exists on the registry, confirm its repository is real and matches the name, and confirm it actually exposes the functions the assistant used, because invented packages usually arrive attached to invented APIs. Teams that use assistants heavily should make this a review norm: a pull request that adds a dependency says where the name came from.

The install script is the delivery mechanism

Whichever route the name arrives by, the payload usually fires the same way. npm runs a package's lifecycle scripts during install by default, so a preinstall or postinstall hook executes arbitrary code the moment the package lands. pip is similar for source distributions, which execute their build code when installed; a wheel, by contrast, is unpacked without running anything. So the fake package does its work on the first machine to install it, with whatever that machine holds: SSH keys and browser sessions on a laptop, deploy tokens and cloud credentials on a build agent.

You can take most of this away with configuration. Set npm to ignore scripts by default and allowlist the few packages that genuinely need them; pnpm has made this the default behaviour since version 10. Prefer wheels in Python and treat source-only packages as needing a closer look. Enforce lockfiles with integrity hashes, installed via npm ci or a frozen-lockfile equivalent, so a name that resolved once keeps resolving to the same bytes. Our note on supply chain risk in your dependencies covers lockfile discipline, release-age cooldowns and provenance in more depth.

Two minutes of checking before every new dependency

None of the controls above requires new tooling, and the per-package habit is genuinely quick. Before a new name goes into a manifest, work through this list.

  • Take the name from the project's own repository or documentation and compare it character by character with what you are about to install, especially if it came from an assistant or a forum answer.
  • Open the registry page and check the publish date, release history, weekly downloads, maintainer list and whether the linked repository really contains this code.
  • Check whether the package declares install scripts, and make sure your package manager will refuse to run them without an explicit allowlist entry.
  • In Python, prefer packages that ship wheels, and slow down on anything that only ships a source distribution.
  • Land the change through a pull request that includes the lockfile diff and a line saying what the package is for and where the name came from.

Where the remaining risk lives

These checks cover the moment one developer adds one package, and they close off most of the naming attacks at the point of entry. The rest of the exposure is structural, and it accumulates quietly: registry and index configuration across every repository and Dockerfile, ownership of your scopes and internal names, which existing dependencies already run install scripts, how broadly CI tokens are scoped, and whether build agents can reach arbitrary hosts or only the registries they need. Mapping that across an estate, and fixing it in an order that reflects blast radius, is the substance of a dependency posture review, and it is work Black Shard does inside secure code review and build hardening engagements. The per-package habit is yours to adopt this week. Start with the next name that lands in a manifest.

Know what your code would give away.

Australia-wide, from our Brisbane head office. Someone will contact you as soon as possible.

Open a briefinfo@blackshard.com.au