Skip to content

The Axios Breach Started with a Plaintext Token

In March 2026, a North Korean state actor (attributed to Sapphire Sleet by Microsoft Threat Intelligence) compromised the axios npm package — 100 million+ weekly downloads — and pushed two backdoored versions that deployed a cross-platform RAT to every machine that ran npm install.

The entire attack chain started with one thing: a long-lived classic npm access token.

The axios project had OIDC trusted publishing configured — the “right” way. But the publish workflow still passed NPM_TOKEN as an environment variable alongside the OIDC credentials. When both are present, npm uses the token. The long-lived token effectively bypassed every security measure the project had in place. The attacker compromised the maintainer’s credentials, used that legacy token to manually publish axios@1.14.1 and axios@0.30.4 with a phantom dependency (plain-crypto-js) that dropped a RAT on install.

One plaintext secret. One of the most widely used packages in the JavaScript ecosystem.

This Is Not a One-Off

28.65 million new hardcoded secrets were pushed to public GitHub repos in 2025 — up 34% year-over-year. 64% of secrets leaked in 2022 were still active in 2026. Nobody rotated them. AI-assisted commits leak secrets at twice the baseline rate. Stolen credentials appear in 31% of all breaches over the past decade.

The common thread: secrets that exist in plaintext — in .env files, in shell history, in CI variables, on disk — are attack surface waiting to be exploited.

The Typical Approach (And Why It Breaks)

.env files (gitignored): The file sits on disk in plaintext. Any malware, any compromised dependency with filesystem access, any postinstall script can read it. One git add -A from a tired developer — or an AI assistant — and it’s in the history forever.

CI/CD secrets: Better for CI, but doesn’t help local development. Secrets are still stored somewhere. Now you have two sources of truth.

Vault/secrets manager: The enterprise answer. Often right for large teams. But for a solo developer managing infrastructure? It’s a lot of overhead for “I need my API token when I run terraform plan.”

What I Actually Do: 1Password CLI + op run

I manage Cloudflare infrastructure across 8 domains using Terraform. That means API tokens, account IDs, zone IDs, R2 storage credentials — a lot of secrets. Here’s the setup. Zero secrets touch disk or git.

The .env.op file (committed to git)

# .env.op — safe to commit. Contains zero secrets.
# These are 1Password references, not values.

CLOUDFLARE_API_TOKEN=op://Private/Cloudflare Terraform/api_token
CLOUDFLARE_ACCOUNT_ID=op://Private/Cloudflare Terraform/account_id
TF_VAR_zone_id_formrecap_com=op://Private/Cloudflare Terraform/zone_id_formrecap_com
AWS_ACCESS_KEY_ID=op://Private/Cloudflare Terraform/r2_access_key
AWS_SECRET_ACCESS_KEY=op://Private/Cloudflare Terraform/r2_secret_key

Every value is an op:// URI — a pointer to a field in a 1Password vault. The file itself contains nothing sensitive. It’s committed to git, reviewed in PRs, and documents exactly which secrets the project needs.

The Makefile (also committed)

OP := op run --env-file=.env.op --

plan:
	$(OP) terraform plan

apply:
	$(OP) terraform apply

op run resolves every op:// reference at runtime, injects the real values as environment variables into the child process, and cleans them up when the process exits. The secrets exist only in memory, only for the duration of the command.

The workflow

make plan    # 1Password prompts for biometric → secrets injected → plan runs
make apply   # same flow

Why This Is Better Than .env Files

Nothing on disk. If the axios RAT had been running on my machine, it would have found .env.op — a file full of op:// URIs that are useless without my 1Password vault and biometric authentication.

Biometric gate. Every op run invocation requires Touch ID. Even if someone has shell access, they can’t exfiltrate secrets without your fingerprint.

One source of truth. Secrets live in 1Password. Period. Rotate the token in 1Password and every make plan picks it up instantly.

Self-documenting. The .env.op file is a manifest of every secret the project needs, which vault it’s in, and what it’s called. New team member? Share the 1Password vault. They clone the repo, run make plan, and it works.

No .gitignore anxiety. The env file is supposed to be committed. There’s nothing to accidentally leak.

It Works With Everything

op run isn’t Terraform-specific. It works with any tool that reads environment variables:

op run --env-file=.env.op -- node server.js
op run --env-file=.env.op -- docker compose up
op run --env-file=.env.op -- wrangler deploy

For CI, 1Password has a GitHub Action that uses a service account and the same op:// references. One OP_SERVICE_ACCOUNT_TOKEN in GitHub secrets. Everything else resolves from 1Password.

The Lesson

The axios maintainer did a lot of things right. OIDC trusted publishing. 2FA. Responsible disclosure. But a single long-lived plaintext token — stored on disk, harvestable by malware — made all of that irrelevant.

The defence isn’t “be more careful with .env files.” The defence is not having secrets in files at all.

op:// references are inert data. They’re a map to a vault — useless without the vault key. The actual secret never touches the filesystem, never appears in shell history, never gets committed to git, and never persists after the process exits.

For anyone managing production infrastructure — or maintaining a package with 100 million weekly installs — that difference is the whole ballgame.