サプライチェーン攻撃対策 - 最低限見直すべき5つのポイント
X-tech推進本部 齋藤npm installを実行する時、何が起きているか意識したことはありますか?
サプライチェーン攻撃「Shai-Hulud」は、その問いを考えさせられるきっかけになりました。
「Shai-Hulud」は公開済みnpmパッケージに悪意あるコードを混入させ、アクセストークンやAPIキーなど(以下「秘匿情報」)が大量に盗まれる攻撃で、これまで第一波(2025年9月 - Shai-Hulud)、第二波(2025年12月 - Shai-Hulud 2.0)、第三波(2026年5月 - Mini Shai-Hulud)と発生してきました。
攻撃の仕組みはシンプルでpackage.jsonのライフサイクルスクリプト(preinstall / postinstallなど)に不正なスクリプトを仕込み、インストール時にあなたの権限で自動実行させるというものです。
この影響範囲はローカルマシンに留まらず、CI/CD環境においても同様です。
これはバグや特殊な脆弱性を突いたわけではなく、npmが持つライフサイクルスクリプトという正規機能を悪用したものです。
これらのスクリプトはパッケージ開発者なら誰でも仕込める仕組みで、npm installを実行するだけで自動的に動きます。
つまりnpm installは単なるダウンロードコマンドではなく、信頼していないコードが実行される入り口でもあるわけです。
自分たちの開発環境の設定を見直してみると、今日からでも対処できることがいくつかありました。
この記事では、今日からすぐ実施できるものから、中長期で取り組みたいものまで、特に効果的な5つの対策を紹介します。
Shai-Hulud の仕組み
GitLab公式ブログ や CISAによるサイバーセキュリティ勧告 によると、攻撃の流れは下記のような内容です。
1. フィッシングでnpmアカウントを奪取
npmやGitHubのアカウント更新を装ったフィッシングメールなど、何らかの方法で、メンテナーの秘匿情報を盗み取ります。
2. 正規パッケージに悪意あるコードを混入・公開
奪ったアカウントで、preinstallフックに悪意あるスクリプトを仕込んだ新バージョンを既存パッケージとして公開します。
スクリプトは表向き正規ツールのインストーラーに見せかけた多段構成になっており、実際の悪意あるペイロードは難読化されたファイルの中に隠されています。
3. インストール時に自動実行・秘匿情報を収集
感染したパッケージをインストールしたユーザーがnpm installを実行した瞬間、悪意あるスクリプトが起動し、以下の情報を収集します。
- GitHub/npmトークン: 環境変数・設定ファイル・CLIの設定から探索
- クラウド認証情報: AWS・GCP・Azureの認証情報を環境変数や設定ファイルから収集
- ファイルシステム全体: 正規のセキュリティツールを悪用してホームディレクトリ全体をスキャン
収集した秘匿情報は、盗んだトークンを使ってインストールしたユーザー自身のGitHubアカウントに公開リポジトリを作成し、そこに送り出されます。 さらにCI/CDのセルフホストランナーをインストールし、侵害後も持続的なアクセス手段を確保します。 感染したシステム同士がトークンを共有するボットネット的な仕組みも持っており、攻撃インフラは非常に耐障害性が高くなっています。
デッドマンズスイッチ
GitHubとnpmの両方へのアクセスが同時に断たれるとユーザーデータの破壊が実行され、ファイルは上書き削除されるため復元はほぼ不可能とのことです。
大規模なテイクダウンが同時多発的なデータ破壊につながりうるという意味でも、対応が難しい設計です。
第三波ではさらにgh-token-monitorという永続化機構が追加されており、GitHubトークンの失効を60秒ごとに監視し、失効を検知した瞬間にホームディレクトリを削除します。
そのため、感染が疑われる場合は秘匿情報のローテーションより先に永続化機構の無効化を行う必要があります。
対応の順序を誤ると、インシデント対応中にデータが失われるリスクがあります。
4. ワームとして自己増殖
盗んだnpmトークンを使って、被害者が管理する全パッケージに同じ悪意あるスクリプトを注入し、バージョンをインクリメントして再公開します。
既存ユーザーが次回npm installを実行した際に自動で感染が広がります。
この「感染 → 窃取 → 増殖」のサイクルがCI/CDのスピードで回り続ける上に、デッドマンズスイッチによる破壊リスクも抱えているのがShai-Huludのいちばん怖いところです。
対策1: ignore-scriptsを有効にする(npmの場合)
手軽さと効果のバランスでいえば、これがもっとも有効です。
postinstallやpreinstallなどのnpmライフサイクルスクリプトをまるごと無効にします。
(参考: npm 公式ドキュメント)
.npmrcで設定する
プロジェクトルートの .npmrc に書けばローカルとCI/CDの両方に適用されます。
ignore-scripts=true
postinstallを必要とするパッケージへの対応
注意点として、セットアップに postinstall を必要とするパッケージは ignore-scripts=true にすると動かなくなる場合があります。
npmにはホワイトリスト機能がないため、代わりに「許可したパッケージだけを明示的にリビルドする」というアプローチが有効です。
postinstall を自前のスクリプトで上書きし、許可リストに含まれるパッケージのみ npm rebuild で再ビルドします。
{
"scripts": {
"postinstall": "npm run rebuild",
"rebuild": "npm rebuild @swc/core sharp --ignore-scripts=false"
}
}
npm rebuildはネイティブビルドのみを実行するため、依存パッケージの任意コードは動きません。
自動実行されるのはこのスクリプトだけで、何が動くかを自分たちで完全に管理できます。
なお、pnpmのようにホワイトリスト機能を持つパッケージマネージャーを使っている場合は pnpm.onlyBuiltDependencies で許可するパッケージを指定するだけで済みます。
npm v12 からはホワイトリスト管理ができるように
2026年7月リリース予定の npm v12 では allowScripts がデフォルト無効となり、スクリプト実行が必要なパッケージは npm approve-scripts コマンドでホワイトリスト管理できるようになります。
現時点では ignore-scripts=true と npm rebuild を組み合わせたアプローチが現実的ですが、v12からは npm のホワイトリスト機能で管理できる見込みです。
(参考: Upcoming breaking changes for npm v12)
対策2: Safe Chainでインストール時のリアルタイム検証を導入する
ignore-scriptsはスクリプトの実行を止めますが、悪意あるパッケージがインストールされること自体は防げません。
一例として、Safe Chain のような、インストール前にパッケージを既知の脅威と照合し、マルウェアをブロックするツールの導入を推奨します。 Safe Chain セットアップ後はnpm・npx・yarn・pnpm・pipなど対応するパッケージマネージャーに自動で保護が掛かり、普段の作業フローをそのまま維持できます。
仕組み
既知のマルウェアはインストール時にブロックします。 加えて、公開後48時間以内のパッケージはデフォルトでブロックされます。 マルウェアが検出・削除されるまでには数時間~数日のタイムラグがあります。その隙をカバーするためです。
公開後一定期間経過していないパッケージをブロックするだけなら npmの min-release-age や pnpmの minimumReleaseAge でも可能です。
対策3: パッケージのバージョンを固定する
悪意あるパッケージが検出・削除されるより前に引き込んでしまうリスクを減らすには、前述のとおり、公開直後のバージョンはインストールしないことも有効ですが、意図せずインストールされてしまうことがないようにバージョンを固定することも効果的です。
lockfileをGit管理しnpm ciでのインストールを厳守する
package.jsonに記載したパッケージのバージョンが範囲指定されている(^ や ~ を使っている)場合、インストールのたびにバージョンが変動する可能性があるため、その時点で該当したバージョンが悪意あるバージョンであっても意図せず引き込んでしまう可能性があります。
package-lock.jsonは、依存パッケージの正確なバージョンを記録したファイルです。
Gitにコミットして、インストールにはnpm installではなくnpm ciを使うことで、記録されたバージョンだけをインストールするため、意図しないバージョンのインストールリスクを防げます。
対策4: 秘匿情報の権限を最小限に絞る
Shai-Huludで被害が拡大した一因は、広いスコープの秘匿情報がCI/CD環境に置かれていたことです。 秘匿情報を盗まれると、それを使って別のパッケージに悪意あるコードを仕込み、感染を広げる踏み台にされてしまいます。
トークンの権限を用途ごとに必要最小限のスコープで分けて発行することが有効です。
そのためには、依存関係のインストール・テスト・成果物の作成・デプロイといった、必要な権限が異なる処理を同一ジョブにまとめず分離することを推奨します。 特にデプロイ系のジョブは、ビルド・テスト系ジョブとは分けるのが基本です。
ジョブを分離することで、次の3つのメリットが得られます。
- 影響範囲の限定: あるジョブが侵害されても横展開を抑えられる
- 最小権限の徹底: 各ジョブに必要な権限だけを付与できる
- 監査しやすさ: どこで何を実行したか追跡しやすくなる
対策5: pnpmへの移行を検討する
pnpmはv10から、依存パッケージのpostinstallスクリプトをデフォルトで無効にしています。
対策1で紹介したignore-scriptsと同じ効果が、設定なしで最初から有効になっているわけです。
さらに、スクリプト実行を許可するパッケージの明示的なホワイトリスト管理や、公開直後のパッケージのブロック(「対策3」相当)、外部ソースからの依存禁止など、この記事で紹介した対策の多くがビルトインで備わっています。npmから移行するだけで、セキュリティの土台が一段上がります。
既存プロジェクトへの影響もあるため、すぐに全面移行する必要はありません。 新規プロジェクトから試してみるのがおすすめです。
(参考: pnpm 公式ドキュメント)
まとめ
当社では次のような取り組みを進めています。
開発環境の統一
セキュリティ設定の一貫性を高めます。 Node.jsやパッケージマネージャーのバージョン管理ツールとしてmiseを導入し社内標準としているほか、パッケージマネージャーをpnpmに統一することも検討しています。 チーム全員が同じセキュリティ設定を自動的に適用できるようになります。
パッケージ使用状況の可視化
各プロジェクトで使用しているパッケージとバージョンを一覧化しているため、侵害が発生した際に影響範囲をすぐに特定でき、対応を迅速に進めることができます。
この攻撃で改めて認識したのは、普段何気なく実行しているnpm installが、知らず知らずのうちに悪意あるコードを動かしてしまう入り口になりうる、ということです。
その意識を持つだけで、日々の開発習慣は大きく変わります。
最初にはじめるとしたら、対策1(ignore-scriptsの有効化)と対策2(Safe Chain の導入)を検討してみてください。
どちらも比較的導入しやすく、postinstallスクリプトを介した攻撃への直接的な盾になります。