Contents
Introduction
Part 1 examined the structural limits of npm’s trust model through the axios supply chain attack. This article covers three defense strategies applied to a real-world project — designed so that even if a supply chain attack does occur, it does not affect us.
Each of the three strategies blocks a different attack surface.
| Strategy | Attack surface blocked |
|---|---|
| GitHub Actions SHA pinning | Tampering with Actions used inside CI |
| Dependabot cooldown | Malicious new versions entering through automated dependency PRs |
Yarn npmMinimalAgeGate | Malicious new versions entering at the time of local or CI install |
The Defense Principle: Bring Time Into the Equation
All three strategies share a single common principle:
Do not immediately accept newly published versions.
As seen in Part 1, supply chain attacks are almost always detected and taken down within hours of publication. Simply waiting a few days means most malicious versions will naturally be filtered out. The essence of these defense strategies is to enforce that simple fact in code and configuration.
The validity of this principle is verified with real incident data in Part 3.
Strategy 1: SHA Pinning for GitHub Actions
What It Blocks
Workflow files typically look like this:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
Tag references like @v6 are readable, but tags are mutable. A maintainer can point the v6 tag at a different commit at any time. If a maintainer’s account is compromised, an attacker can do the same.
This is exactly what happened in 2025, when the tags of the widely-used tj-actions/changed-files were moved to a malicious commit, causing numerous workflows to leak secrets (CVE-2025-30066).
To make this concrete: the single tag reference
- uses: tj-actions/changed-files@v45
in a workflow file became the entry point for RCE (Remote Code Execution) — allowing an attacker to run arbitrary commands inside our CI environment. Our code was untouched, but the moment that one line pointed to the attacker’s malicious commit, the next CI run executed that malicious code with the full permissions of the workflow. Workflows typically have access to deployment tokens, cloud credentials, npm tokens, GitHub PATs, and other secrets stored as environment variables. A single line of tag mutation could hand all of those secrets directly to the attacker.
Solution: Pin to a SHA
The fix is simple: reference an immutable commit SHA instead of a tag.
- - uses: actions/checkout@v6
+ - uses: actions/checkout@<commit SHA> # v6
A SHA is the hash of the commit’s content. Once a SHA is set, the code it points to can never change. Even if a maintainer moves the tag, our workflow keeps running the exact commit we reviewed and approved.
The readability loss can be mitigated by leaving a version comment after the SHA.
Scale of the Change
All 50+ CI workflow files in the project were converted to SHA references.
The scope covers everything from official GitHub Actions like actions/checkout, actions/setup-node, and actions/cache to third-party Actions like aws-actions/configure-aws-credentials and tj-actions/changed-files.
How to Look Up a SHA
The most tedious part of the initial setup is finding out which SHA a given tag points to. The following command does it quickly:
# Look up the SHA that the v6 tag points to
git ls-remote --tags https://github.com/actions/checkout.git v6
To look up which tag corresponds to a SHA, do the reverse:
git ls-remote --tags https://github.com/tj-actions/changed-files.git \
| grep <commit SHA>
Compatibility with Dependabot
A natural question is: “If I pin to a SHA, how do I get updates?” The answer: Dependabot correctly detects and creates PRs for updates to SHA-pinned Actions. When a new version is released, a PR automatically opens with the SHA updated, and a human can review and merge the change.
The only costs are some readability loss and the one-time effort of looking up SHAs. After the initial setup, automation keeps working exactly as before.
Strategy 2: Dependabot Cooldown
What It Blocks
Dependabot automatically creates update PRs whenever a dependency has a new version. It’s a great tool for receiving security patches quickly, but it is also the fastest pipeline for ingesting malicious new versions.
As seen in Part 1, the window of exposure for a supply chain attack is typically just a few hours. If Dependabot creates a PR immediately after a new version is published, and someone merges that PR — or CI simply installs the dependencies for it automatically — malicious code is inside the environment.
Solution: A 7-Day Waiting Period
GitHub Dependabot introduced a cooldown option around 2025. When enabled, Dependabot will only create a PR for a new version after a specified amount of time has passed since it was published.
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
cooldown:
default-days: 7
semver-major-days: 7
semver-minor-days: 7
semver-patch-days: 7
What each option means:
| Option | Description |
|---|---|
default-days | Default waiting period applied to all updates |
semver-major-days | Waiting period for major version updates |
semver-minor-days | Waiting period for minor version updates |
semver-patch-days | Waiting period for patch version updates |
Seven days is not an arbitrary number. Analysis of supply chain incident data shows that the vast majority of known attacks are detected and removed within seven days. The reasoning is covered in detail in Part 3.
Security Alerts Are Immediate
One important note: security alerts are exempt from cooldown. Update PRs for CVEs with published advisories are created immediately, regardless of the cooldown setting. In other words, “wait 7 days for new versions, but patch known vulnerabilities right away” — which is exactly the right behavior.
Strategy 3: Yarn 4’s npmMinimalAgeGate
What It Blocks
Dependabot cooldown delays the point at which a PR is created. But it does not apply when a developer adds a dependency directly — for example, by running yarn add some-package while introducing a new library. Cooldown has no effect in that case.
The gap is filled by blocking installation of new versions directly at the package manager level.
Solution: Block at Install Time
Starting with Yarn 4.10, the npmMinimalAgeGate option was introduced. When set, any package version that has not been published to the npm registry for at least the specified period cannot be installed.
# .yarnrc.yml
npmMinimalAgeGate: 7d
This single line consistently applies a 7-day gate to both yarn install and yarn add. It behaves the same way on a developer’s local machine and in CI.
To verify the setting is applied correctly:
yarn config get npmMinimalAgeGate
# 10080 (= 7 days × 24 hours × 60 minutes)
Why Upgrading to Yarn 4 Was Part of the Plan
Using this option required upgrading from Yarn 3.7.0 to Yarn 4.14.1. The upgrade itself brings a number of security improvements as a side benefit.
| Improvement | Description |
|---|---|
enableScripts defaults to false | Third-party package postinstall scripts are disabled by default |
| Hardened Mode | Validates the integrity of the lockfile and package metadata |
npmMinimalAgeGate | Restricts installation of newly published packages (primary goal of this change) |
| npm metadata caching | Approximately 4x improvement in install speed (separate from security benefits) |
Of particular note: enableScripts: false blocks postinstall by default — the very execution trigger at the heart of the axios attack described in Part 1. The axios attack depended on plain-crypto-js’s postinstall running. In a Yarn 4 environment, that step simply does not happen.
Migration Considerations
Upgrading to Yarn 4 requires a small amount of housekeeping:
- Update the
packageManagerfield inpackage.json([email protected]→[email protected]) - Regenerate
yarn.lockin v9 format (this produces a large diff) - Replace the
--frozen-lockfileflag in CI workflows with--immutable(the--frozen-lockfileflag was removed in Yarn 4)
Because the lockfile change is large, it is worth communicating in advance that other feature branches will need a rebase after the merge.
How the Three Strategies Divide Responsibility
The three strategies each block a different entry point, so they are only meaningful when applied together.
| Entry point | 1. SHA pinning | 2. Dependabot cooldown | 3. npmMinimalAgeGate |
|---|---|---|---|
| Tampered Action in a workflow | O | ||
| New dependency version via Dependabot PR | O | O | |
New library added with yarn add | O | ||
New version pulled in by yarn install in CI | O |
Applying Strategy 2 or Strategy 3 alone is already enough to block malicious versions that would be caught and removed within 7 days — like the axios case. Using both together adds a second gate: one at the PR stage and one at the install stage.
Conclusion
The three strategies discussed in this article all apply the same principle — do not immediately accept newly published versions — using different tools at different points in the pipeline.
None of them are particularly sophisticated or complex. They amount to a few lines of configuration and some workflow cleanup. What makes the change worthwhile is the data: just how many attacks this simple approach can actually block.
The next article, Effectiveness of Supply Chain Defense Strategies, addresses “Is 7 days really enough?”, “How many known supply chain incidents could these defenses have blocked?”, and “What attacks do these strategies fail to stop?”
References
- Dependabot cooldown official documentation
- Yarn 4 migration guide
- Yarn
npmMinimalAgeGatedocumentation
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku.Deku created the applications with Flutter.If you have interested, please try to download them for free.