Skip to main content

Command Palette

Search for a command to run...

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

Updated
4 min readView as Markdown

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.