# How npm typosquatting and dependency confusion attacks work (and how to stop them)

In February 2021, security researcher Alex Birsan published a paper describing how he had successfully deployed malicious packages to the internal build systems of Apple, Microsoft, PayPal, Shopify, and 32 other companies. He did it without hacking a single server, exploiting a single CVE, or phishing a single developer. He published packages to npm, PyPI and RubyGems with names matching internal packages those companies used — and their build systems installed his public versions automatically.

This is **dependency confusion**. It doesn't have a CVE because the vulnerability isn't in the package — it's in the resolution logic of package managers. And it's just one flavour of supply-chain attack that pure CVE-based scanners completely miss.

## Typosquatting: catch the typo, own the build

**Typosquatting** exploits the fact that developers type package names by hand. Publish `expresss` to npm (with three s's) and anyone who makes that common typo in `npm install` pulls down whatever you put in the package. Publish it with a convincing README, a few hundred weekly downloads from bots, and a malicious `postinstall` script, and you have a stealthy credential-stealing payload reaching developer machines and CI pipelines.

Real examples from npm's history:

- `crossenv` — a typosquat of `cross-env` that stole environment variables (which often contain API keys and tokens).
- `getcookies` — embedded in a legitimate-looking utility, exfiltrated browser cookies.
- `loadsh` — typosquat of `lodash`, one of the most-downloaded npm packages.
- `reqeusts` / `reqests` — multiple typosquats of the Python `requests` library.

The `postinstall` script is the key mechanism: npm runs it automatically when the package installs, before you've had a chance to read the code. A malicious `postinstall` can exfiltrate environment variables, install persistent backdoors, or modify other packages in `node_modules`.

## Dependency confusion: the namespace collision attack

Dependency confusion exploits a different logic flaw. Many companies use a private npm registry (Artifactory, Nexus, GitHub Packages) alongside the public npm registry. When a package manager is told to look up a package, most configurations check the public registry if the private one doesn't have it — or check both and take the highest version.

Birsan's attack: find the name of an internal package (often visible in job postings, leaked config files, or error messages), publish a public package with the same name at a version number higher than the internal one (e.g. `99.0.0`), and wait for build systems to install the "newer" public version.

The fix at the package manager level is to scope all internal packages under a private namespace (`@company/internal-lib` rather than `internal-lib`) and configure the registry to only allow scoped packages from the private registry. But this requires intentional configuration that many teams never got around to.

## What detection looks like

CVE-based scanners can't catch these attacks because there's no CVE. The signals that matter:

**Typosquat detection** uses edit distance algorithms (Levenshtein, Damerau-Levenshtein) to find packages in your dependency tree whose names are suspiciously close to popular packages but aren't those packages. A package one character transposition away from `lodash`, `express` or `react` with no established publishing history is a strong signal.

**Install-script presence** is a risk signal on its own. Legitimate packages rarely need to run code at install time. A package with a `postinstall` script from an unknown publisher warrants scrutiny, especially if it appeared recently in your tree.

**Maintainer and publish history** signals: a package published 3 days ago with 0 dependents and a `postinstall` script that matches a popular name is almost certainly malicious or a testing mistake. Package maintainer changes on established packages — the XZ Utils backdoor mechanism — are similarly high-signal.

**Version anomalies**: a version number that skips from `1.0.4` to `99.0.0` is a classic dependency confusion marker.

## DepWarden's approach

DepWarden flags typosquats using a Damerau-Levenshtein edit-distance check against a curated list of the most popular packages in each ecosystem. A package one keystroke away from a popular one that isn't that package gets a supply-chain finding (HIGH severity) with the specific match it resembles.

Install-script presence, maintainer-change signals and version anomalies from the OpenSSF Scorecard and supply-chain metadata are also surfaced — the signals that have no CVE but matter enormously in the real attack landscape.

The important point: these attacks happen before a CVE is ever filed, if one ever is. By the time there's a CVE for `loadsh`, the attack has already run. The only defence is catching the suspicious signal upstream.

Curious how many of these look-alike names are actually live on the registry right now? We checked every single-character typo of the 30 most popular npm and PyPI packages against the real registries and cross-referenced the hits against OSV's malicious-package database — see [the full data](https://depwarden.in/blog/npm-pypi-typosquatting-2026-report).

