In late March 2026, compromised axios builds briefly appeared on the npm registry (for example 1.14.1 and 0.30.4 on affected release lines). Attackers added a malicious dependency and used lifecycle scripts so a npm install could pull down far more than an HTTP client — a pattern we have seen before in registry incidents, not a bug in axios’ normal code.
The bad versions were taken down quickly, but any machine or CI job that installed them in that window should be treated as potentially affected: rotate secrets where relevant, check install logs and lockfiles, and pin to known-good versions (axios at or below 1.14.0 / 0.30.3 on those lines, per vendor and security advisories).
A small npm setting that helps with “brand new” malware
npm (recent CLI versions) supports min-release-age: installs only dependency versions that have been published at least N days ago. That does not stop every attack, but it cuts exposure to hijacks where the malicious tarball exists only for hours before npm removes it.
npm config set min-release-age 3
Here 3 means three days (see npm config: min-release-age). You can also pass it per command: npm install --min-release-age=3.
Trade-offs: you need a recent npm CLI (this landed in the v11 line; run npm -v and check the config docs for your version). Installs can fail if no published version is “old enough.” It is one layer among many — lockfiles, pinning, and reviewing postinstall still matter — but for “hot” malicious publishes, a few days’ delay helps.
Use npm ci in your pipeline, not npm install
This incident highlighted something easy to miss: if your CI pipeline runs npm install, it hits the live npm registry on every run and re-resolves version ranges from scratch. That is the window an attacker needs.
If your package.json says ”axios”: “^1.7.9”, then npm install can pull in 1.14.1 — a version that was never in your lockfile. That is exactly how many people picked up the malicious build published on March 31.
Why npm ci is safer
npm ci installs exactly what is in package-lock.json. If the lockfile does not match package.json, the command fails rather than silently resolving something new. It never reaches out to the registry for a fresher version. A newly published malicious package simply cannot get in until you explicitly update the lockfile yourself.
Swap it in your workflow:
# Before
- name: Install dependencies
run: npm install
# After
- name: Install dependencies
run: npm ci
Bonus: it is also faster
npm ci wipes node_modules and reinstalls from scratch without trying to reconcile the existing tree. In practice that is 20–40 % faster than npm install in a clean CI environment.
One line change, and your pipeline stops silently adopting whatever just appeared on the registry. Keep your lockfile committed and update it deliberately — do not let CI do it automatically.
Hardening GitHub Actions workflows
The axios incident is about npm, but your GitHub Actions workflow itself is another attack surface. The tj-actions compromise in 2025 showed exactly this: an attacker re-pointed a version tag to malicious code, and any workflow using that tag ran it automatically.
Pin actions to a commit SHA, not a tag
Tags like @v3 or @v40 are mutable — they can be deleted and pointed somewhere else. A 40-character commit SHA cannot be changed after the fact.
# Before (vulnerable — tag can be moved)
uses: tj-actions/changed-files@v40
# After (safe — this exact commit, forever)
uses: tj-actions/changed-files@2d756ea93da014e7b7df225d13f5e6e43e5c2ee7 # v40.0.2
Do this for every third-party action in your workflows. First-party actions (actions/checkout, actions/setup-node) from GitHub are lower risk but still worth pinning.
To keep SHAs up to date without manual work, add Dependabot for Actions:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Dependabot will open PRs with updated SHAs automatically.
Use least-privilege permissions
By default, GITHUB_TOKEN has more permissions than most jobs need. Lock it down at the workflow level:
permissions:
contents: read
Then grant extra permissions only to the specific job that needs them. If a compromised action runs in your workflow, it can only do what the token allows.
Replace long-lived secrets with OIDC
Static credentials (AWS keys, npm tokens, Docker Hub passwords stored as secrets) are valid for months. If they leak, the attacker has months to use them.
OIDC gives each workflow run a short-lived token (15–60 minutes) instead. No static secret to steal.
For AWS:
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@e3dd6d6512e493a47ee3ea56a9890a770ddb8787 # v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/YOUR_ROLE
aws-region: us-east-1
Azure and Google Cloud support the same pattern with their respective actions.
Monitor what your workflow actually does at runtime
Even with SHA pinning, a compromised action could make unexpected network calls or write files outside expected paths. Harden-Runner from StepSecurity adds runtime monitoring to your jobs:
steps:
- uses: step-security/harden-runner@v2
with:
egress-policy: audit
Start with audit mode to see what network calls your workflow makes normally. Once you have a baseline, switch to block to deny anything unexpected.
Protect your tags
If your release workflow publishes to npm or deploys on a tag push, protect those tags. In repository settings, add a tag protection rule for v* and require signed tags. This stops an attacker with limited repo access from creating a fake release tag and triggering your publish workflow.
Summary
None of these are silver bullets on their own. The axios incident got people who ran npm install without a lockfile. The tj-actions incident got people who trusted mutable tags. The pattern is always the same: implicit trust in something that turned out to be mutable.
| What to do | Why |
|---|---|
Use npm ci in CI |
Locks to your lockfile, no surprise upgrades |
Pin exact versions in package.json |
No caret ranges that resolve to new versions |
Set min-release-age |
Skips packages published in the last N days |
| Pin Actions to commit SHAs | Tags can be moved, SHAs cannot |
| Use OIDC instead of static secrets | Short-lived tokens limit blast radius |
Least-privilege permissions |
Limits what a compromised action can do |
| Enable Harden-Runner | Catches unexpected behaviour at runtime |