目次
はじめに
第1回ではaxios供給チェーン攻撃の事例を通じて、npmの信頼モデルが抱える構造的な限界を確認しました。今回は、供給チェーン攻撃が実際のプロジェクトで起きないように — 正確には、起きたとしても影響を受けないように — 実際のプロジェクトに適用した3つの防御戦略をコードとともに解説します。
ここで紹介する3つの戦略は、それぞれ異なる攻撃対象領域を防ぎます。
| 戦略 | 防ぐ攻撃対象領域 |
|---|---|
| GitHub Actions SHAピン固定 | CIで使用するActions自体の改ざん |
| Dependabot cooldown | 自動依存関係PRを通じた新しい悪意あるバージョンの流入 |
Yarn npmMinimalAgeGate | ローカル・CIのインストール時点での新しい悪意あるバージョンの流入 |
防御の原則: 時間を味方につける
3つの戦略に共通する原則はシンプルです。
新しく公開されたものをすぐに受け入れない。
第1回で見たように、供給チェーン攻撃は公開から数時間以内に検出・削除されるパターンが圧倒的です。つまり、数日待てばほとんどの悪意あるバージョンは自然とふるい落とされます。このシンプルな事実をコードと設定で強制するのが、今回適用した防御戦略の本質です。
この原則の有効性は第3回で実際のインシデントデータとともに検証します。
戦略1. GitHub ActionsのSHAピン固定
何を防ぐか
GitHub Actionsのワークフローは通常、次のように書かれています。
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
@v6のようなタグ参照は可読性が高いですが、**タグは変更可能(mutable)**という問題があります。メンテナーがv6タグが指すコミットを別のコミットに移動させることができます。メンテナーアカウントが乗っ取られれば、攻撃者も同様にタグが指すコミットを変更できます。
実際に2025年には、ダウンロード数の多いtj-actions/changed-filesのタグが悪意あるコミットに書き換えられ、多数のワークフローがシークレットを漏洩したインシデントがありました(CVE-2025-30066)。
もう少し具体的に説明すると、ワークフローファイルに記述した
- uses: tj-actions/changed-files@v45
というたった1行のタグ参照が、攻撃者が私たちのCI環境で任意のコマンドを実行できる**RCE(Remote Code Execution、リモートコード実行)**の入り口になったのです。私たちのコードは変わっていないにもかかわらず、その1行が指すコミットが攻撃者の悪意あるコミットに置き換わった瞬間から、次のCI実行でその悪意あるコードがワークフローの権限で実行されます。ワークフローは通常、デプロイトークン、クラウド認証情報、npmトークン、GitHub PATなどの環境変数にアクセスできるため、この1行の改ざんだけでそれらすべてのシークレットが攻撃者の手に渡る可能性があります。
解決策: SHAで固定する
解決策はシンプルです。タグの代わりに**不変(immutable)**なコミットSHAで参照すればよいのです。
- - uses: actions/checkout@v6
+ - uses: actions/checkout@<コミットSHA> # v6
SHAはコミット内容のハッシュなので、一度決めたSHAが指すコードは絶対に変わりません。メンテナーがタグを移動させても、私たちのワークフローは私たちが検証したその時点のコミットを使い続けます。
可読性が下がるデメリットは、SHAの後にバージョンコメントを残すことで補えます。
適用規模
実際のプロジェクトで使用するすべてのCIワークフロー50本あまりをSHA参照に変換しました。
対象はactions/checkout、actions/setup-node、actions/cacheのようなGitHub公式Actionsから、aws-actions/configure-aws-credentials、tj-actions/changed-filesのようなサードパーティActionsまですべて含みます。
SHAの調べ方
初回適用時に最も手間がかかるのが「このタグが指すSHAは何か」を調べる作業です。次のコマンドで簡単に確認できます。
# v6タグが指すSHAを調べる
git ls-remote --tags https://github.com/actions/checkout.git v6
逆にあるSHAに対応するタグを確認するには次のようにします。
git ls-remote --tags https://github.com/tj-actions/changed-files.git \
| grep <コミットSHA>
Dependabotとの互換性
「SHAで固定すると更新はどうするの?」という疑問が湧くかもしれません。結論から言えば、DependabotはSHAでピン固定されたActionsの更新も正常に検知してPRを作成します。新バージョンが出るとSHAを自動で更新したPRが開き、人がその変更をレビューした上でマージできます。
つまり、可読性の低下とSHA調査の手間を除けば、運用上の負担は増えません。一度設定しておけば自動化のメリットはそのまま享受できます。
戦略2. Dependabot cooldownの設定
何を防ぐか
Dependabotは依存関係に新バージョンが出ると自動で更新PRを作成します。セキュリティパッチを素早く受け取るための優れたツールですが、同時に悪意あるバージョンを最も素早く取り込む通路にもなります。
第1回で見たように、供給チェーン攻撃の露出ウィンドウは通常数時間です。Dependabotが新バージョン公開直後にPRを作成し、誰かがそのPRをマージすれば — あるいは単にCIがPRの依存関係を自動インストールするだけでも — 悪意あるコードが環境に入り込みます。
解決策: 7日間の冷却期間
GitHub Dependabotは2025年頃からcooldownオプションを提供しています。このオプションを有効にすると、新バージョンが公開されてから一定時間が経過しないとDependabotがそのバージョンへのPRを作成しません。
# .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
各オプションの意味は次のとおりです。
| オプション | 説明 |
|---|---|
default-days | すべての更新に適用されるデフォルトの待機日数 |
semver-major-days | メジャーバージョン更新の待機日数 |
semver-minor-days | マイナーバージョン更新の待機日数 |
semver-patch-days | パッチバージョン更新の待機日数 |
7日という数字は任意に決めたものではありません。供給チェーン攻撃の事例分析によれば、既知のインシデントのほとんどが7日以内に検出・削除されています。詳しい根拠は第3回で解説します。
セキュリティアラートは即時
ここで1つ注意点があります。セキュリティアラートはcooldownの対象外です。CVEが公開された脆弱性に対するセキュリティ更新PRは、cooldown設定に関係なく即座に作成されます。つまり「新バージョンは7日待つが、既知の脆弱性はすぐパッチ」という合理的な動作が実現できます。
戦略3. Yarn 4のnpmMinimalAgeGate
何を防ぐか
Dependabot cooldownは「PRが作成されるタイミング」を遅らせます。しかしPRを経由せずに直接依存関係を追加する場合 — たとえば開発者が新しいライブラリを導入する際にyarn add some-packageを実行する場合 — cooldownは機能しません。
この隙間を埋めるのが、パッケージマネージャー自体で新バージョンのインストールを防ぐ機能です。
解決策: yarn installの時点でブロック
Yarn 4.10からnpmMinimalAgeGateオプションが導入されました。このオプションを設定すると、npmレジストリに公開されてから指定した期間が経過していないパッケージバージョンはインストール自体がブロックされます。
# .yarnrc.yml
npmMinimalAgeGate: 7d
このたった1行でyarn install、yarn addのいずれにも一貫して7日ゲートが適用されます。ローカルの開発者PCでもCIサーバーでも同じように機能します。
設定が正しく適用されているかは次のコマンドで確認できます。
yarn config get npmMinimalAgeGate
# 10080 (= 7日 × 24時間 × 60分)
Yarn 4アップグレードを一緒に行った理由
このオプションを使うために、既存のYarn 3.7.0をYarn 4.14.1にアップグレードしました。アップグレード自体が副次的にいくつかのセキュリティ改善をもたらします。
| 改善事項 | 説明 |
|---|---|
enableScriptsのデフォルトfalse | サードパーティパッケージのpostinstallスクリプトがデフォルトで無効化 |
| Hardened Mode | lockfileとパッケージメタデータの完全性検証 |
npmMinimalAgeGate | 新規公開パッケージのインストール制限(今回の変更の主目的) |
| npmメタデータキャッシング | インストール速度が約4倍向上(セキュリティとは別のメリット) |
特にenableScripts: falseは、第1回で見たaxios攻撃の核心的な実行トリガーだったpostinstallをデフォルトで防ぐという点で重要です。axios攻撃はplain-crypto-jsのpostinstallが実行されることで成立しますが、Yarn 4環境ではその段階自体がなくなります。
マイグレーション時の注意点
Yarn 4アップグレードには若干の追加作業が必要です。
package.jsonのpackageManagerフィールドを更新([email protected]→[email protected])yarn.lockをv9形式で再生成(大規模な差分が発生)- CIワークフローの
--frozen-lockfileフラグを--immutableに置き換え(Yarn 4では--frozen-lockfileは廃止)
lockfileの変更が大きいため、マージ後に他の作業ブランチのrebaseが必要になることを事前に共有しておくとよいでしょう。
3つの戦略の役割分担
3つの戦略はそれぞれ異なる侵入口を防ぐため、組み合わせて適用することで初めて意味を持ちます。
| 侵入口 | 1. SHAピン固定 | 2. Dependabot cooldown | 3. npmMinimalAgeGate |
|---|---|---|---|
| ワークフローのActions改ざん | O | ||
| Dependabotが作成した新依存関係PR | O | O | |
yarn addによる新ライブラリの導入 | O | ||
CIでyarn install実行時の新バージョン流入 | O |
特にaxiosのような事例に当てはめると、2番と3番のどちらか一方があるだけでも、7日以内に検出・削除される悪意あるバージョンがインストールされる事態は防げます。両方を適用したのは「PRの段階」と「インストールの段階」という異なるタイミングで二重にゲートをかける効果を狙ったものです。
まとめ
今回解説した3つの戦略はすべて、新しく公開されたものをすぐに受け入れないという同じ原則を、異なるツール・異なるタイミングで適用したものです。
特別に精巧だったり複雑な手法があるわけではありません。設定ファイル数行とワークフローの整理で完結します。それでもこの変更が意味を持つ理由 — 単純な変更が実際にどれだけ多くの攻撃を防げるか — はデータにあります。
次の記事供給チェーン防御戦略の有効性では、「7日という数字は本当に十分なのか?」「最近知られている供給チェーンインシデントのうち、この防御で防げたものはいくつか?」「そしてこの戦略が防げない攻撃は何か?」を整理します。
参考資料
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Dekuが開発したアプリを使ってみてください。Dekuが開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。