Safeguard
Supply Chain

cross-env and the crossenv Typosquat: A Supply Chain Case Study

In 2017, a malicious crossenv package on npm stole environment variables from developers who mistyped cross-env. The incident is still the cleanest case study in typosquatting we have.

Priya Mehta
DevSecOps Engineer
7 min read

The cross env npm story is really two stories: a legitimate, boringly useful utility called cross-env, and a malicious 2017 typosquat called crossenv that stole environment variables from anyone who dropped the hyphen. The incident was small in absolute terms — npm counted at most a few dozen real victim installs — but it remains the cleanest teaching case in software supply chain security, because every element of the modern typosquat playbook appeared in it: name proximity, an install-time payload, credential exfiltration, and full preservation of the real package's functionality so nobody noticed. This post walks through the legitimate package, the attack, and the defenses that have (and haven't) improved since.

The real package: what cross-env does

cross-env (by Kent C. Dodds) solves a genuinely annoying portability problem: setting environment variables in npm scripts works differently on Windows than on POSIX shells. NODE_ENV=production node build.js fails on classic Windows shells; cross-env makes one syntax work everywhere:

{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
  }
}

That is the whole package. It became a fixture in frontend build tooling with millions of weekly downloads, which is exactly what made its name valuable to squat. (Worth knowing in 2025: the project describes itself as in maintenance mode — it is considered done rather than abandoned — and on modern Node you can often replace it with --env-file flags or shell-neutral script runners. Fewer dependencies is always the cheapest win.)

The attack: crossenv, July–August 2017

On July 19, 2017, an npm account named hacktask published crossenv — no hyphen — along with a batch of other lookalike names (babelcli, d3.js, mongose, jquery.js, and more; npm's cleanup ultimately removed about 40 packages). The crossenv package was the dangerous one, and its design shows why typosquats succeed:

  1. It ran at install time. The package declared an install-hook script in its package.json, so simply running npm install crossenv executed the payload — no import, no require, no usage needed.
  2. It stole the highest-value, lowest-effort target: process.env. The payload serialized the developer's entire environment — which in real life contains npm tokens, AWS keys, database URLs, CI secrets — base64-encoded it, and sent it via HTTP POST to an attacker-controlled host (npm.hacktask.net).
  3. It then behaved perfectly. crossenv wrapped the real cross-env, so builds kept working. Nothing crashed, nothing looked wrong. The malware ran once and left a fully functional tool behind.

The packages sat on the registry for about two weeks until Swedish developer Oscar Bolmsten noticed the payload and flagged it publicly on August 1. npm removed the batch the same day and published a postmortem: crossenv had been downloaded nearly 700 times, with an estimated 50 or fewer real (non-mirror, non-scanner) installs. Snyk later catalogued it as CVE-2017-16074.

Why this tiny incident became the canonical case study

Because it demonstrated, in miniature, the economics that still drive npm supply chain attacks. Publishing a typosquat costs an attacker nothing. Detection depended on a volunteer reading code. Each successful install yielded credentials that could cascade — an exfiltrated npm token lets the attacker publish malicious versions of the victim's packages, converting one typo into a downstream compromise of every consumer. And functionality-preserving wrappers defeat the only detection most teams had: "does the build still pass?"

Every major npm incident since — the 2018 event-stream maintainer-trust attack, the waves of install-script credential stealers, the 2024–2025 campaigns against CI environments — reuses some combination of these moves. The crossenv incident is just the version small enough to hold in your head.

The defenses that actually map to this attack

Block or gate install scripts. The payload only worked because npm executes lifecycle scripts on install by default. npm install --ignore-scripts (or ignore-scripts=true in .npmrc) neutralizes this entire class at the cost of breaking the minority of packages that legitimately need postinstall builds. pnpm made the stronger call: it does not run dependency lifecycle scripts unless you explicitly allowlist them per package. If you adopt one control from this article, this is it.

Make typos hard to commit. Lockfiles mean a typo has to survive code review once, not every install. Adding dependencies via CLI (npm install cross-env) still typos, so the review layer matters: a diff introducing a new package name deserves an actual look, and dependency-review automation can compare new names against popular-package edit distances — precisely how crossenv-style names get caught pre-merge.

Starve the payload. The attack stole process.env because developer and CI environments were (and mostly still are) buffets of long-lived secrets. Short-lived credentials (OIDC-federated cloud auth in CI), scoped npm automation tokens, and secret managers that inject at use-time rather than exporting everything into the shell all shrink what any install-time malware can grab.

Scan for malicious packages, not just vulnerable ones. Classic CVE matching would never flag crossenv before Snyk catalogued it. Modern software composition analysis adds registry-intelligence feeds: known-malicious package lists, install-script behavior flags, typosquat-distance warnings, and publish-anomaly signals. An SCA platform such as Safeguard applies those checks across every lockfile in the org, which matters because the developer who typos is rarely the one reading security advisories.

Verify provenance where it exists. npm's package provenance attestations (build-transparency for packages published from public CI) postdate this incident and would not have stopped it directly — crossenv was its own "legitimate" package — but provenance raises the bar for the adjacent attack of hijacking a real package's publishing pipeline.

What has and hasn't changed since 2017

npm now runs automated malware detection, name-similarity rules block some lookalike registrations (spurred directly by this incident), and 2FA is enforced for high-impact maintainers. Those changes killed the laziest version of the attack. What hasn't changed: lifecycle scripts still run by default with your full user privileges, the registry still accepts thousands of new packages daily, and the gap between "published" and "detected" is still measured in days or weeks — the recent large-scale campaigns confirm attackers moved from passive typo-waiting to active phishing and dependency confusion rather than abandoning the space. The supply chain track in Safeguard Academy covers the full lineage from crossenv to today's campaigns if you want the deeper history.

The durable lesson is unglamorous: npm install is arbitrary code execution by design. Treat every new dependency name as an input requiring validation, and every install environment as one that will eventually run something hostile.

FAQ

Is cross-env (with the hyphen) safe to use?

Yes. cross-env is the legitimate utility by Kent C. Dodds and was never compromised — the 2017 attack was a separate lookalike package named crossenv. The real package is in maintenance mode but stable and enormously widely used.

What did the crossenv malware actually do?

On install, it ran a lifecycle script that collected the machine's environment variables via process.env, base64-encoded them, and POSTed them to an attacker-controlled server, then wrapped the real cross-env so everything kept working. It was live for about two weeks in July–August 2017 before npm removed it and roughly 40 sibling packages.

How do I protect a team from npm typosquatting today?

Disable automatic install scripts (pnpm's default, or ignore-scripts with npm), require review for new dependency names, keep long-lived secrets out of developer and CI environments, and run an SCA tool with malicious-package intelligence rather than CVE matching alone.

Do I even still need cross-env?

Often not. Modern Node.js supports --env-file, and many scripts can avoid inline environment variables entirely. Where you do need cross-platform env setting in npm scripts, cross-env remains the standard answer.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.