Build Stack Review

npm Typosquatting Attack Patterns and Detection in CI Pipelines

Coordinated attackers now automate typosquatting to steal CI secrets at scale.

· 11 min read
Cover illustration for “npm Typosquatting Attack Patterns and Detection in CI Pipelines”
Package and Dependency Auditing · October 11, 2026 · 11 min read · 2,574 words

npm typosquatting has moved from a scattered nuisance to a coordinated discipline built on automation infrastructure, and that shift changes what you need to defend against it. The old pattern was simple: an attacker registered a misspelled package name and waited for someone to fat-finger npm install. That approach rarely worked, and once someone noticed, people cleaned it up fast. Microsoft's analysis describes something far more deliberate: a single threat actor, operating under a freshly created maintainer identity, published 14 malicious packages inside a four-hour window, all aimed at the same cluster of OpenSearch, ElasticSearch, DevOps, and environment-configuration tooling. One actor built one campaign with one credential-harvesting payload compiled across every package, so this looks like a production run, not a one-off experiment. SANDWORM_MODE, documented in February 2026, pushed the model further still, with 19 packages, a deployed MCP server, and prompt-injection layered on top of ordinary credential theft, showing that attackers now assemble campaign infrastructure.

The naming tricks attackers use to get a package installed by mistake

The deception starts at the character level, and it works because each trick mimics an error a tired or distracted developer makes on a normal day. Omission drops a letter, so lodash becomes lodas. Transposition swaps two adjacent letters, turning requests into reqeusts. Delimiter confusion swaps a separator character, as happened in 2017 when cross-env became crossenv, a package that pulled in nearly 700 downloads before npm removed it, though npm itself estimated the real number of affected installs at no more than 50. Homoglyph substitution goes a step further by swapping in a character that looks identical to the original: an uppercase "I" in place of a lowercase "l" renders Iodash indistinguishable from lodash in most terminal fonts. None of these tricks work in isolation from the surrounding metadata. A package can spoof a plausible repository link and carry a high version number, and that gives an automated scanner or a registry page reader exactly the two signals it tends to check, so both can be faked without triggering a single alarm. A live audit of registered single-character typo variants across the 30 most popular npm packages found that roughly a third already carry a confirmed malicious advisory in OSV's public database, meaning these names are registered, populated, and documented as harmful today.

Lifecycle Hooks and Silent Code Execution

The deeper danger is npm's install-time execution model, not in the package name itself. npm's preinstall, install, and postinstall hooks run automatically the moment a dependency resolves, with the privileges of whatever process is doing the installing, and none of that requires the application to ever call require() on the malicious code. The vpmdhaj cluster shows the mechanics in full. In its first generation, a preinstall hook fires preinstall.js, which gathers host context (hostname, platform, architecture, Node version, the USER/USERNAME environment variable, the working directory, npm_package_name, and npm_package_version), base64-encodes the result, and sends it to a command-and-control endpoint tagged with a campaign-specific header, X-Supply: 1. The server answers with a compressed second-stage binary that gets written to disk as payload.bin, marked executable, and launched detached from the install process. The second generation of the same campaign dropped that noisy round-trip entirely: a script called setup.mjs downloads the legitimate Bun runtime straight from a real GitHub release, then runs a second-stage payload that was already bundled inside the npm tarball, removing the outbound call to a C2 server that defenders had started watching for. A persistence shim in index.js re-launches payload.bin every time the module gets required again, so the infection survives across CI build stages and developer rebuilds, and no fresh install is needed. SANDWORM_MODE added a timing dimension on top of this: its Stage 2 payload fired immediately inside CI environments but sat dormant for 48 to 96 hours on developer machines to slip past short-lived sandbox analysis, while Stage 1 credential harvesting ran immediately everywhere. Attackers are now tuning payload behavior to the specific environment they expect to land in.

Diagram: How a Lifecycle Hook Executes Before Your App Ever Runs. Visualizes: Show the two-generation attack sequence documented in the vpmdhaj cluster.

Why CI Pipelines Are the Preferred Target

CI runners hold what these payloads are built to steal, and they install dependencies on a schedule with no human in the loop to notice anything odd. A developer laptop generally carries personal access tokens. A CI runner carries publish tokens, cloud deployment keys, and broad access to production secrets, and all of it is scoped wide so automated jobs can work without asking permission at every step. A stolen npm publish token is especially dangerous because it is reusable: an attacker who gets hold of one can push a poisoned update to every package the compromised account maintains, spreading the campaign to downstream consumers without ever registering a new typosquatted name. The vpmdhaj payload targets AWS IMDS v2 and ECS task-role endpoints specifically, infrastructure that exists in cloud-hosted CI and production, not on a developer's laptop. SANDWORM_MODE's design, firing instantly in CI while sitting dormant on developer machines, is the clearest signal that the pipeline is the actual objective and that compromising a developer's local environment is, at best, a secondary goal. Standard tooling offers little help here: npm audit checks for known vulnerabilities tied to CVEs, and a typosquatted package is malicious by design rather than by bug, so it carries no CVE and produces no signal in that audit.

AI-Assisted Coding and MCP Toolchains Expand the Attack Surface

AI coding agents can install dependencies on their own, and that changes what a typosquatted name has to fool. The attacker is no longer exploiting a human typing error, but the package-name intuitions baked into a language model. In an AI-assisted workflow, an LLM suggests or installs a package directly, and if it generates a name that sounds plausible but is wrong, a pattern researchers call slopsquatting, the install happens with no human review and no moment where a developer might catch the misspelling before it runs. SANDWORM_MODE extended this further by deploying a malicious MCP server on any machine that installed one of its packages. That server used embedded prompt injection to manipulate AI assistants that later connected to it through MCP, so it harvested credentials from inside the AI toolchain, not from the package's own runtime. MCP tool schemas raise the stakes again, because they are natural-language descriptions read by the model at the moment it chooses a tool, not text a human developer reviews first. If an attacker writes a tool description that matches a model's intuition better than the legitimate package's description, they can win that selection step without using a single character-level typo, so what used to be a string-matching problem becomes a semantic one that string-based defenses cannot catch.

What the lockfile actually prevents and where it stops protecting you

The lockfile is the single most important supply-chain control a team can enforce in CI, provided it was clean before the attack reached it and provided the pipeline uses the right install command to respect it. npm ci treats the lockfile as the final word: it installs what package-lock.json resolves, never modifies the lockfile, and fails outright if package.json and the lockfile disagree. npm install behaves differently and can silently update the lockfile to a newer version that still satisfies a version range, which is precisely the mechanism by which a poisoned name can slip into a build that looked clean the day before. The caveat that matters most: if a developer ran npm install locally with a typosquatted name already sitting in package.json, the resulting lockfile will faithfully reproduce that malicious package on every subsequent npm ci run. The lockfile stops accidental upgrades. It does nothing to stop poisoning that happened before it was generated. Version range discipline compounds this problem or fixes it, depending on how a team writes its dependencies: an exact pin like 1.2.3 removes automatic resolution entirely, while a caret range like ^1.2.0 still allows silent resolution to an unreviewed version even inside a lockfile-based workflow, so exact versions or tilde ranges are the more defensible choice for production applications. A complementary control arrived in July 2025, when Dependabot added native support for minimum package age configuration: teams can set something like a 24-hour hold before a newly published version gets pulled in, so the npm security team and the community get a window to flag and remove a malicious release first. Environment drift can undercut all of this in ways that have nothing to do with malice: on Ubuntu 24.04 with Node 22 and npm 10, npm ci --ignore-scripts succeeds, but on the same runner image with Node 24 and npm 11, the same command fails because npm 11 treats the lockfile as out of sync over missing platform-specific entries. A CI host moving between npm major versions can fail before tests even run, or a developer can regenerate a materially different lockfile without realizing it, weakening the exact reproduction the lockfile is supposed to guarantee.

Detection controls that belong directly in the CI pipeline

Typosquatted packages carry no CVE and slide past standard audit tooling, so catching them calls for a set of controls built directly into the pipeline, with each one aimed at a different signal the attack leaves behind.

Disabling lifecycle hooks during dependency installation removes the most direct execution path, since preinstall, install, and postinstall scripts are how both vpmdhaj and SANDWORM_MODE launch their payloads. Some legitimate packages genuinely need these scripts, for native add-ons or build steps, so the practical approach is to audit which packages actually require them and allowlist those explicitly rather than letting every hook in every dependency run by default.

Splitting install permissions from deployment permissions limits what a hook can reach even if it does run. The install step needs no publish token, no cloud key, and no deploy credential, so putting install and build into one CI job with permissions: contents: read and publish or deploy into a separate job means a lifecycle payload firing during installation lands in a job that holds nothing worth stealing. Pair this with a federated short-lived token scheme for publish jobs, instead of long-lived npm tokens; doing so limits the damage even if a token is stolen.

Typosquat-aware name scanning at install time catches what hook-blocking and permission splitting cannot, which is a legitimate-looking name that was never supposed to be in package.json to begin with. Cross-referencing candidate names against OSV's malicious-package advisories confirms which names carry a documented malicious record, separating them from the larger population of short names that happen to collide for innocent reasons, and you need both checks together to make the scan useful in production. This check can run as a pre-install hook or as its own CI step that validates every name in package.json and the lockfile before npm ci runs.

Outbound network monitoring during the install phase catches the mechanism the Gen-1 vpmdhaj stager depends on: a C2 beacon sent during npm install, tagged with the distinctive header X-Supply: 1, which Microsoft identifies as a high-confidence signal in proxy logs. Restrict outbound access from install-phase jobs to known-good registries, the npm registry itself or a private mirror, and you catch that beacon while also making the Gen-2 Bun-runtime download stand out as an unexpected connection to a GitHub Release endpoint. Gen-2 was built specifically to beat defenders who only watch for unusual outbound traffic, so relying on network egress monitoring alone will miss it, and name scanning and script blocking have to run alongside it.

Provenance and integrity verification give teams a way to answer questions after the fact. A content-hashed, deterministic lockfile lets a team identify whether a specific build was exposed to a malicious package version without rescanning every artifact that was ever produced.

Registry access controls round out the layer. Routing installs through a private registry mirror that enforces an allowlist of approved names closes off the open-publishing surface entirely for teams able to manage that overhead, and scoping internal packages under a private organization prefix, with npm configured to resolve that scope only from the private registry, prevents dependency-confusion attacks, where an attacker publishes a public package that matches an internal name and npm resolves to the wrong one.

Reproducible, Pinned Environments Reduce the Window of Exposure

Detection controls fail when they are applied unevenly, and that inconsistency creates the gaps these campaigns exploit. If a developer runs one npm version locally while CI runs another, or if lockfiles drift between machines, the review a team believes happened may not match what actually gets installed. The npm version mismatch described earlier is a case in point: when a developer regenerates a lockfile on a different npm major version than CI uses, the lockfile the pipeline trusts no longer represents what anyone reviewed, and that mismatch stays invisible until something breaks or a malicious package gets through. Pinning the full tool environment, not just package versions but the Node version, the npm version, and any native dependencies, ensures npm ci always runs against a lockfile generated by the same toolchain that produced it. A detection control is only as strong as the consistency of the environment running it. Declarative environments that define tool versions in a readable manifest and reproduce identically across developer laptops, CI runners, and deployment targets close the "works on my machine" gap where a malicious package installs locally but is never caught because CI resolves dependencies down a different path. When an environment is defined declaratively with input-derived hashes, a CVE discovered after deployment can be traced directly to the affected build environments and blocked or patched at the hash level, with no need to rescan every image or dig through logs to figure out what was exposed. A software bill of materials generated from a deterministic dependency graph is ready for audit the moment it is produced rather than reconstructed after the fact, which means a question like "were we running the malicious version of this package between these dates" has a definite answer instead of a best guess.

The detection posture teams should leave with

Diagram: The Minimum Effective Defense Stack. Visualizes: Visualize the five-control minimum posture stated at the article's conclusion as an ordered, layered stack: (1) npm ci in every pipeline job — lockfile as source of truth; (2)…

Stopping industrialized npm typosquatting means treating dependency installation as a security-sensitive operation at every stage of the pipeline, not a problem developer caution already solved. The attack history makes clear that campaigns adapt to each control in isolation: Gen-2 stagers exist because defenders started watching outbound network calls, CI-immediate and developer-delayed activation exists because sandbox analysis runs on a short clock, and spoofed metadata exists because developers and automated scanners alike read registry pages before deciding to install. The minimum effective posture that follows from this evidence runs npm ci in every pipeline job, applies --ignore-scripts at install time with an explicit allowlist for the packages that genuinely need hooks, runs name scanning with edit-distance matching against malicious-package databases as its own pre-install gate, separates install permissions from publish and deploy permissions so install jobs hold no secrets worth stealing, and sets a minimum release age buffer on automated dependency updates. Teams running AI-assisted development workflows carry an added obligation: audit AI assistant configurations for unauthorized MCP server entries, and treat anything an agent installs with the same scrutiny given to a manual install, since every control above applies equally to both paths. None of it holds together without one underlying guarantee: a reproducible, version-pinned environment running identically from developer laptop through CI to production, so that every one of these detection controls is checking the same resolved dependency graph at every stage, with no drift left open for a campaign to exploit.

Sources

  1. Typosquatted npm packages used to steal cloud and CI/CD secrets

More in Package and Dependency Auditing