Axios got backdoored: 5 npm packages to replace with HTTP APIs
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 installis 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
postinstallhooks 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.