Amazon Traced the chalk and axios Hijacks to One Group. Two npm Settings Answer It

Amazon Threat Intelligence linked four npm compromises to a single state-backed actor, all of them starting with a socially engineered maintainer. The defence is not a scanner: it is four documented npm config options, and each one has a cost worth naming.

Two lines in your .npmrc change more about your npm supply chain risk this week than any tool you could buy: ignore-scripts=true, which stops dependencies executing code when they install, and min-release-age=7, which refuses any version published in the last seven days. Both are documented npm config options. Both are off by default. Neither costs anything.

The reason to spend the twenty minutes now is an attribution Amazon Threat Intelligence published this week, tying four separate npm package compromises to one state-linked group and showing that the entry point in each case was a person, not a registry bug.

The four packages, and what they had in common

Amazon's security blog post links the compromises of typo-crypto (March 2025), debug and chalk (September 2025) and axios (March 2026) to a DPRK-linked actor it tracks as SAPPHIRE SLEET, known elsewhere as STARDUST CHOLLIMA, BlueNoroff, CageyChameleon and Alluring Pisces. Amazon notes that axios alone has over 100 million weekly downloads.

Three details from that post matter more than the attribution itself.

The access route was social engineering. Amazon says the actor "gained access by socially engineering a trusted maintainer of the package, then published a software update containing malicious code." No typosquat, no registry breach. The package name in your package.json was correct, the maintainer account was the real one, and the only thing wrong was the version number.

The execution route was install hooks. The shared techniques Amazon lists include "post-install hooks (scripts that run automatically when a package is installed)". That is the part most teams forget: npm install is not a download, it is a program that runs other people's programs.

The payload is now split across packages. Amazon describes a shift away from single malicious packages: "One package stores an encrypted blob disguised as configuration. A second ships the decryption logic." Each fragment passes a static scan and a human skim on its own.

Amazon also published indicators: the domain npmjs[.]store, the address 216[.]74[.]123[.]126, and the identifier MAL-2026-3400 in the OSV database. Search your egress logs and your OSV feed for those three before you do anything else.

Why your lockfile did not help

A committed package-lock.json pins the tree you already resolved, and npm's config documentation confirms it is honoured by default (package-lock defaults to true). That protects you from silent drift. It does nothing on the day you or Dependabot bump a version, which is exactly the day a hijacked release is fresh. And it does nothing about install scripts, which run whether the version came from a lockfile or not.

The four settings, and what each one costs

Config optionDefaultWhat it preventsWhat it costs you
ignore-scriptsfalsenpm does not run scripts specified in package.json files, so a dependency's preinstall, install and postinstall hooks never executePackages that compile a native addon at install time (the node-gyp cases) will not build until you rebuild them explicitly
min-release-agenullOnly versions "available more than the given number of days ago" are installed, so a release published this morning cannot enter your treeGenuine security patches wait the same number of days unless you override
beforenullRebuilds the tree so only versions available on or before a given date are installedA frozen point in time; useful for reproducing an incident, not for day-to-day development
foreground-scriptsfalseNothing on its own, but runs preinstall, install and postinstall in the foreground sharing stdio, so you see what executedLouder logs, slower installs

All four definitions come from npm's own config reference. The costs in the right-hand column are ours, from what each option does.

What the delay actually buys

min-release-age is a bet that somebody else notices first. The value you set is the number of days you are willing to be behind in exchange for not being in the first cohort to install a hijacked release. Seven days is a reasonable starting point for application repositories; a library you ship to other people can justify more.

The honest caveat is that this cuts both ways. A fix for an actively exploited vulnerability in a dependency also waits seven days under that setting, and that is a worse outcome than the risk you were avoiding. So write the override down before you need it, as a one-line escape hatch your on-call person can run without thinking:

npm install [email protected] --min-release-age=0

Put the same policy in the repository, not in someone's shell:

# .npmrc, committed
ignore-scripts=true
min-release-age=7
foreground-scripts=true

Putting back the builds you just broke

Turning off install scripts globally will break the handful of dependencies that legitimately need them. The fix is to allow them one at a time rather than switching the setting back off. After install, rebuild only what you have looked at:

npm ci
npm rebuild better-sqlite3 sharp

Keep that list in the repository next to the .npmrc, and treat additions to it as a code review, because every name on it is a package you have agreed may run arbitrary code on your build machine. This is the same discipline we argued for with model downloads in pinning every model your build downloads, and it fails for the same reason when it lives in one engineer's memory instead of in version control.

If you publish packages, the token is the target

Everything above is defence for consumers. If you maintain a package, the compromise you should be planning against is the one Amazon described: your account, not your code. npm's trusted publishing removes the long-lived token from the equation entirely by using OIDC from CI. The specifics, from npm's documentation:

  • Supported on GitHub Actions (GitHub-hosted runners), GitLab CI/CD (GitLab.com shared runners) and CircleCI cloud. Self-hosted runners are not supported yet.
  • Requires npm CLI 11.5.1 or later and Node 22.14.0 or higher.
  • One trusted publisher per package, configured at npmjs.com under Packages, then your package, then Settings, then Trusted Publisher.
  • GitHub Actions workflows need the id-token: write permission.
  • Provenance attestations are generated and published automatically from GitHub Actions and GitLab CI/CD. CircleCI does not currently support provenance.

The gap trusted publishing does not close is the one that was actually used here. If an attacker persuades a maintainer to merge a change, the malicious code goes through CI and gets a provenance attestation proving it was built exactly where it claims. Provenance answers "did this come from the repository", not "should this code be in the repository". Branch protection and a second reviewer on release commits are what answer the second question, and neither is an npm setting. Our note on what build tools push into your output covers the adjacent failure where the secret leaves in the artifact rather than through the account.

The order to work through this afternoon

  1. Grep egress logs and dependency alerts for npmjs[.]store, 216[.]74[.]123[.]126 and MAL-2026-3400.
  2. Add the three-line .npmrc above to your application repositories and commit it.
  3. Run npm ci and note every build that now fails; those are your install-script dependencies.
  4. Add a npm rebuild line naming exactly those packages, and nothing else.
  5. Write the --min-release-age=0 override into your incident runbook.
  6. For each package you publish, configure a trusted publisher and delete the classic token it replaces.

If you only do one of these, do the first line of the second one. A dependency that cannot execute code at install time is a dependency that has to wait until you actually call it, and that is a materially smaller problem than the one Amazon just described. Teams running their own servers should pair it with the open port review, because install-time execution and an exposed service are the two ends of the same afternoon.

Discussion

Sign in with Google or just a name. No email link, no password to remember.