Reducing package supply-chain risk with release-age checks and lockfiles
On this page5 sections ▾
The axios npm compromise prompted me to look at two controls teams can add to their dependency workflow: a minimum release age before adopting a version, and lockfiles checked into source control. They reduce different risks and work best alongside restrictions on install scripts and regular dependency review.
I joked about this on LinkedIn by comparing modern package installs with the old shared folder of DLLs maintained by "Dave from accounting". Package managers make updates easier, but a compromised maintainer account or a malicious dependency can distribute code through that same update path.
#What happened with axios
On March 31, 2026, attackers used a compromised axios maintainer account to publish malicious packages. Google Threat Intelligence Group attributed the attack to UNC1069, a North Korea-linked threat actor. They changed the account email and published two backdoored versions: 1.14.1 and 0.30.4. Both included a malicious dependency called plain-crypto-js that silently downloaded and executed a cross-platform RAT called WAVESHAPER.V2.
The malicious versions were live for about 2-3 hours before npm pulled them. That sounds like a small window, but axios gets around 100 million weekly downloads. Wiz reported finding the compromised versions in roughly 3% of the cloud environments they scanned. That figure describes the environments Wiz scanned, rather than all axios users.
A delay before adopting new releases can help with attacks discovered shortly after publication.
#Defense 1: Release-age gating
The concept is simple. Don't install any package version that was published less than a certain number of days ago. A version removed within that window should never become eligible for a new resolution under that policy. The delay gives maintainers and researchers time to identify problems; it doesn't establish that an older package is safe.
The following settings use a seven-day delay. Check that your installed package-manager version supports the option, and apply it in CI as well as locally.
#npm
min-release-age=7npm's min-release-age setting uses days. Put it in your project's .npmrc so the team shares the setting. You can also add ignore-scripts=true to block postinstall scripts, which is how the axios payload executed.
#pnpm
minimum-release-age=10080pnpm uses minutes: 10,080 minutes is seven days. The .npmrc form above applies to pnpm 10. For newer versions, use minimumReleaseAge: 10080 in pnpm-workspace.yaml; pnpm's configuration documentation explains which settings have moved out of .npmrc. pnpm 10 also restricts dependency install scripts by default. Keeping the malicious dependency's script unapproved would prevent that install-time execution path.
#Bun
[install]
minimumReleaseAge = 604800Bun uses seconds: 604,800 seconds is seven days.
#uv (Python)
exclude-newer = "7 days"uv accepts human-readable duration strings. You can also use an exact RFC 3339 timestamp if you want to pin to a specific point in time.
For package managers installing axios, an enforced seven-day delay would exclude those newly published versions from resolution during the incident. The uv example applies the same approach to Python packages; it doesn't install axios. These settings don't remove a package that is already installed or establish that versions already in a lockfile are trustworthy.
#Defense 2: Lockfiles and hash pinning
Release-age checks delay new versions. Lockfiles record the versions selected for a build, and integrity hashes help detect downloaded content that differs from the recorded artifact. A lockfile can still faithfully reproduce a malicious version if that version was selected when the lockfile was created.
Package managers such as npm, pnpm, Cargo and uv record package versions and artifact hashes in their lockfiles. Installs can then check downloaded package content against the recorded hash and fail on a mismatch. Integrity checking establishes that content matches the lockfile; it doesn't check whether the code is benign.
The rules are straightforward:
- Commit your lockfile.
package-lock.json,pnpm-lock.yaml,bun.lock,uv.lock,Cargo.lock,packages.lock.json. All of them belong in source control - Use frozen installs in CI.
npm ci,pnpm install --frozen-lockfile,bun install --frozen-lockfile. This makes CI use exactly what's in the lockfile, nothing more - Never force past a hash mismatch. If
EINTEGRITYshows up, something changed. Investigate, don't--force
#What about .NET and Rust?
For NuGet and Cargo, lockfiles, source controls and audit tools provide other useful checks:
.NET / NuGet has several layers of protection:
- Lock files -- enable
RestorePackagesWithLockFilein your.csprojand useRestoreLockedModein CI to fail on lockfile drift - Central Package Management --
Directory.Packages.propscentralizes all version declarations and can pin transitive dependencies - Package signature verification -- NuGet.org repository-signs all packages, and you can verify with
dotnet nuget verify - NuGet Audit -- built into
dotnet restoresince .NET 8, warns about known vulnerabilities - Package Source Mapping -- restricts which NuGet source each package can come from, preventing dependency confusion attacks
Rust / Cargo is in a similar spot:
- Cargo.lock -- stores SHA-256 checksums for every crate, verified on install
- cargo-vet -- Mozilla's audit tool that records human review of crate versions, with shareable audit databases across organizations
- cargo-audit -- scans against the RustSec Advisory Database for known vulnerabilities
- cargo-deny -- checks licenses, bans specific crates, and verifies sources
These tools cover different checks: signatures and hashes establish provenance or consistency, audit databases report known vulnerabilities, and review records document human assessment. None establishes that every dependency is safe.
#The checklist
For a project adopting these controls:
- Add release-age gating to your package manager config (if your ecosystem supports it)
- Commit your lockfiles to source control
- Use frozen installs in CI, no resolving, no surprise updates
- Run audit tools regularly (
npm audit,pnpm audit,dotnet restorewith NuGet Audit,cargo audit) - Review new dependencies before adding them. Check download counts, maintainer history, and recent activity
Check that CI actually uses the committed lockfile and the intended release-age policy. If an urgent security fix falls inside the waiting period, review that update explicitly rather than disabling the policy for every package.
Thanks to Brook Jeynes for sharing the package manager configs that inspired this post. He had something in mind but didn't have capacity to write it up, so here we are.