Skip to content
guide

Axios got backdoored: 5 npm packages to replace with HTTP APIs

| 9 min read
Server room with network cables representing supply chain infrastructure
Photo by Taylor Vick on Unsplash
Server room with network cables representing supply chain infrastructure
The axios maintainer's laptop sat inside your CI job. Smaller dependency surface, fewer laptops that can compromise your build. Photo by Taylor Vick on Unsplash

On March 31, 2026, a North Korea-linked operator published two backdoored versions of axios to npm. Versions 1.14.1 and 0.30.4 shipped with a postinstall dependency named plain-crypto-js that pulled a platform-specific RAT implant from sfrclak[.]com:8000. The packages were live for three hours. Axios gets 70 million weekly downloads. Do the math.

Every CI job that ran npm install during that window shipped a payload into its build environment. Secrets in environment variables got exfiltrated; private GitHub tokens got used to mint follow-on releases; developer laptops running npm install locally got a RAT. The incident bypassed 2FA because the maintainer's machine was already compromised through a targeted social-engineering campaign. 2FA on the npm account does nothing when the attacker owns the terminal that runs npm publish.

You cannot eliminate this class of attack, but you can shrink your blast radius. Every npm package that does one small thing; validating an email, parsing a phone number, stripping HTML, generating a QR code, signing a JWT; is a package you can delete and replace with an HTTPS call to an API you control via rotatable key. A compromised package runs before you can react. A compromised API key stops working in seconds.

Here are five npm packages you can replace this week, with the HTTP API that does the same thing, plus a CI guard that blocks new postinstall hooks from landing in your lockfile.

Audit your current npm dependency surface

Before you delete anything, know what you have. This shell pass surfaces production dependencies with install hooks and checks whether the poisoned axios versions are anywhere in your tree:

# 1. List production deps with install hooks
npm ls --omit=dev --all 2>/dev/null | awk '{print $1}' | while read pkg; do
  scripts=$(npm view "$pkg" scripts 2>/dev/null)
  if echo "$scripts" | grep -qE "postinstall|preinstall"; then
    echo "HOOK: $pkg"
    echo "$scripts"
    echo "---"
  fi
done

# 2. Check axios is not the poisoned version
npm ls axios | grep -E "1\.14\.1|0\.30\.4" && echo "ROTATE SECRETS NOW"

# 3. Freeze your lockfile and enable provenance
npm config set provenance true
npm config set package-lock true

If either axios version appears in your lockfile, rotate every secret the affected machine or CI job had access to. Not "check the logs." Rotate. Assume exfiltration happened in the 180 minutes between publication and takedown. Secrets that leaked cannot be unleaked.

Google Threat Intelligence attributes the operation to UNC1069, the same actor that has run earlier WAVESHAPER campaigns. Their playbook targets maintainer laptops via phishing, then rides the maintainer's credentials to publish. Your defense has to assume that at least one maintainer of at least one transitive dep is one phish away from being an implant delivery system.

Replacement 1: email-validator → /v1/email/validate

validator, email-validator, deep-email-validator, and disposable-email-domains show up in a huge share of signup flows. Together they add roughly half a megabyte of install weight, their own transitive trees, and a maintained list of disposable domains that goes stale within weeks.

curl -X POST https://api.botoi.com/v1/email/validate \
  -H "Content-Type: application/json" \
  -d '{"email": "finance@acme-corp.com"}'
{
  "data": {
    "email": "finance@acme-corp.com",
    "valid": true,
    "format": true,
    "mx": true,
    "disposable": false,
    "domain": "acme-corp.com",
    "mx_records": ["aspmx.l.google.com"]
  }
}

The API checks syntax, DNS MX records, and a live disposable-domain list in one call. Swapping the two packages out looks like this:

// Before: pulls validator (~460 KB) plus disposable-email-domains (~30 KB)
import validator from "validator";
import disposable from "disposable-email-domains";

function isBusinessEmail(email) {
  if (!validator.isEmail(email)) return false;
  const domain = email.split("@")[1];
  return !disposable.includes(domain);
}

// After: zero dependencies, MX check included
async function isBusinessEmail(email) {
  const res = await fetch("https://api.botoi.com/v1/email/validate", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.BOTOI_API_KEY,
    },
    body: JSON.stringify({ email }),
  });
  const { data } = await res.json();
  return data.valid && !data.disposable;
}

You lose the ability to validate offline. You gain a maintained MX check, a live disposable list, and zero packages with postinstall hooks sitting in your tree. For signup, checkout, and webhook email fields, that trade-off favors the API.

Replacement 2: libphonenumber-js → /v1/phone

libphonenumber-js is a port of Google's libphonenumber. It weighs 147 KB minified. The "mini" build drops to 79 KB but loses metadata for most countries. The full metadata bundle brings the weight back to 2 MB unpacked. On Cloudflare Workers or a cold Lambda, this is real latency that you pay on every invocation.

curl -X POST https://api.botoi.com/v1/phone \
  -H "Content-Type: application/json" \
  -d '{"number": "+14155552671"}'
{
  "data": {
    "valid": true,
    "e164": "+14155552671",
    "country": "US",
    "country_code": 1,
    "national_number": "4155552671",
    "type": "mobile",
    "carrier": null,
    "timezone": ["America/Los_Angeles"]
  }
}

One POST returns E.164 format, country, national number, line type, and timezone. If the number is invalid, data.valid is false and the rest is null. Your server-side signup flow calls this between "read form" and "write to database." The 60 ms API round trip sits inside your existing DB write window.

Replacement 3: qrcode → /v1/qr/generate

The qrcode npm package itself is fine: 34 KB, no postinstall hook. What is not fine is the canvas peer dependency that half of the tutorials push you toward, which wraps native code, installs node-gyp, and has ten years of CVEs across its build toolchain. Every native npm dep is a supply-chain seam.

curl -X POST https://api.botoi.com/v1/qr/generate \
  -H "Content-Type: application/json" \
  -d '{"text": "https://acme-corp.com/order/8412", "ecc": "M"}' \
  > order-qr.svg

The response is raw SVG. Pipe it to a file, stuff it into an invoice template, or render it inline in a React component with dangerouslySetInnerHTML. No native modules, no build toolchain, no transitive tree.

Replacement 4: jsonwebtoken → /v1/jwt/generate and /v1/jwt/decode

jsonwebtoken is one of the most-copied JWT libraries in Node. It is also the library most people misconfigure: wrong algorithm, missing audience claim, no expiry. A wrong-algorithm verify call plus an attacker-controlled header reintroduces the 2015-era JWT none vulnerability. The API enforces algorithm whitelists and rejects unsigned tokens at the endpoint:

# Generate
curl -X POST https://api.botoi.com/v1/jwt/generate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $BOTOI_API_KEY" \
  -d '{
    "payload": {"sub": "user_42", "role": "admin"},
    "secret": "'"$SIGNING_SECRET"'",
    "expires_in": 3600
  }'

# Decode (inspect claims, check is_expired)
curl -X POST https://api.botoi.com/v1/jwt/decode \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $BOTOI_API_KEY" \
  -d '{
    "token": "eyJhbGciOi..."
  }'

Use this for short-lived tokens issued by a backend service: password-reset links, one-time download URLs, service-to-service bearer tokens. Do not use an external service for user session JWTs on the hot path of every authenticated request; for those, keep a verified library in-process and lock the algorithm.

Replacement 5: html-to-text → /v1/html-to-text/convert

html-to-text, sanitize-html, node-html-parser, and their friends exist because every API accepting user content eventually needs to strip HTML for plain-text previews, email digests, or search indexing. Their combined weight is 500 KB to 1.2 MB; they pull parse5 or htmlparser2 through the door, which each have their own maintainer surface.

curl -X POST https://api.botoi.com/v1/html-to-text/convert \
  -H "Content-Type: application/json" \
  -d '{"html": "<p>Order <strong>#8412</strong> shipped to 123 Main St.</p>"}'
{
  "data": {
    "text": "Order #8412 shipped to 123 Main St."
  }
}

For a richer output, /v1/html-to-markdown returns GitHub-Flavored Markdown, and /v1/html-sanitize returns cleaned HTML with a configurable allowlist. Pick the one that matches how your downstream consumer wants to store the content.

Add a CI guard that blocks new postinstall hooks

Deleting packages is one-time work. Staying lean is continuous. This GitHub Actions check fails the PR if a lockfile update introduces a new postinstall or preinstall hook anywhere in the tree:

# .github/workflows/install-guard.yml
name: install-guard
on: [pull_request]

jobs:
  block-postinstall:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Fail on new postinstall hooks
        run: |
          git fetch origin ${{ github.base_ref }}
          diff=$(git diff origin/${{ github.base_ref }} -- package-lock.json)
          if echo "$diff" | grep -E '^\+.*"(postinstall|preinstall)"'; then
            echo "::error::New postinstall/preinstall hook introduced. Review manually."
            exit 1
          fi

The check is cheap, runs on every PR, and forces a human review before any new script hook lands. Pair it with npm config set ignore-scripts true on CI and explicit allowlists for packages you know need esbuild-style postinstall (TypeScript, Puppeteer, bcrypt).

What you gave up, and when to put it back

Moving validation to an HTTP call has real costs. You are trading them for a smaller dependency surface:

Trade-off npm package HTTP API
Latency Microseconds 50 to 150 ms from edge
Offline use Yes No
Install risk Postinstall runs arbitrary code No install step
Revocation Rebuild, republish, redeploy Rotate API key in seconds
Audit trail None by default Request log per call
Version drift Pin or renovate Version header, stable contract

The right answer is "both, with intent." Keep critical-path libraries in-process (session JWT verification, crypto primitives, auth middleware). Move the long tail of single-purpose utilities out of your lockfile and onto a signed HTTPS endpoint that you can revoke.

Key takeaways

  • Assume a maintainer is one phish away from an implant. Axios got 70M weekly downloads and still had its npm account compromised through a social-engineering attack on a personal laptop.
  • Audit postinstall hooks today. Any dep that runs arbitrary code during npm install is a supply-chain seam. Inventory them, then delete or allowlist.
  • Delete single-purpose packages first. Email, phone, QR, JWT signing, HTML conversion; each has a one-line HTTP replacement with a revocable key.
  • Rotate, do not investigate, after exposure. If poisoned axios landed in your tree, rotate every secret the affected environment touched. Exfiltration happens in minutes.
  • Add a CI guard. Block new postinstall hooks from landing in your lockfile without a human review. The axios compromise would have tripped this guard.

Botoi provides HTTP replacements for the five packages above and about 145 more single-purpose utilities: hashing, UUID generation, regex testing, timestamp conversion, JSON schema validation, barcode generation, PDF rendering, and the rest. One API key, 5 req/min on the free tier, no install hooks. Browse the interactive docs or wire the MCP server into your AI coding agent to call the same endpoints from Claude Code or Cursor without leaving the editor.

Frequently asked questions

What happened with the axios npm package in March 2026?
Between March 31, 2026, 00:21 and 03:20 UTC, an attacker used a compromised maintainer account to publish axios 1.14.1 and 0.30.4 with a malicious postinstall dependency called plain-crypto-js. The dependency downloaded platform-specific RAT implants from sfrclak[.]com:8000. Google Threat Intelligence attributes the operation to UNC1069, a North Korea-nexus actor. The packages were live for roughly three hours, long enough to land in CI caches and developer laptops worldwide.
Does replacing npm packages with HTTP APIs reduce risk?
It shrinks the attack surface in two ways. First, you remove a postinstall hook that runs arbitrary code on your build server. Second, you move validation logic off the developer laptop and into a signed, HTTPS-only endpoint that you control via API key rotation. A poisoned package runs before you can react; a revoked API key stops working in seconds.
Is an HTTP call slower than a local npm package?
For a single call on a cold request, yes; typical botoi API latency is 50 to 150 ms from a client in North America to the Cloudflare edge. For most server-side flows (signup, checkout, webhook processing) that overlap with DB calls you already make, it adds nothing measurable. For high-throughput paths, cache the response by input hash for the same latency profile as a local package.
How do I audit my repo for risky npm packages right now?
Run npm audit --omit=dev to surface production dependencies, then inspect any package with a postinstall or preinstall hook using npm ls and npm view {name} scripts. Packages that do one small thing (email validation, QR generation, JWT signing, phone parsing, HTML stripping) are strong candidates to move to an HTTP call. Packages that do cryptography or actively fetch network content are the highest-priority audit targets.
What if an HTTP API provider gets compromised instead of npm?
The blast radius is smaller and the detection faster. You control the API key and can revoke it in one call. Your provider exposes a status page, an incident RSS, and signed responses over HTTPS. Compare that to a package running inside your build where detection requires reading every postinstall hook in your transitive dependency tree. Neither is zero risk; one gives you levers, the other does not.

Try this API

NPM Package Info API — interactive playground and code examples

More guide posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.