先に結論だけ
重要な前提:悪意あるnpmパッケージのpreinstallスクリプトが実行された時点で、ローカル環境(ソースコードやユーザーディレクトリ以下のファイル)はすでに侵害されています。この記事で紹介する対策は、ローカル環境の感染を防ぐものではなく、GitHubリポジトリへの感染拡大の阻止策です。
開発体験を維持しつつ、npmサプライチェーン攻撃(shai-hulud型)からGitHubリポジトリへの感染拡大を防ぐには、以下の3つの設定を組み合わせます
- GitHub Fine-Grained PAT:最小権限の原則(Repository Rulesetsを変更不可)
- 1Password Shell Plugin:コマンド実行ごとに人間の認証が必要
- スコープ設定:コーディングエージェントと認証状態を共有しつつ、セキュリティを維持
2層の防御メカニズム
第1層 1Password Shell Plugin
攻撃スクリプトが gh コマンドを実行しようとしても、1Password認証プロンプトが表示されるため、人間の承認なしではコマンドを実行できません。
第2層 Fine-Grained PAT
仮に gh コマンドの実行が許可されても、最小権限により致命的な操作(リポジトリの新規作成・削除、Repository Rulesetsの変更、GitHub Actionsワークフローの改ざん等)を阻止できます。
運用上の利点
1PasswordはFine-Grained PATを簡単に切り替えられます。これにより、大きな権限を持つGitHub設定用のPATと、日常的に利用する開発用の最小権限PATを分離できます。PAT切り替えのストレスをなくすことで、最小権限PATの運用がしやすくなります。
はじめに
npmサプライチェーン攻撃は2025年も継続しており、開発者の認証情報を狙った攻撃が進化しています。2025年に観測されたshai-hulud 2.0マルウェアは、npmパッケージのpreinstallフックを悪用し、GitHub認証情報を自動的に盗み取ります。
この記事では、shai-hulud型攻撃の手法を解説し、その攻撃に対して1Password Shell Plugin + GitHub Fine-Grained PATがどのように感染拡大防止に働くかを紹介します。
対象読者
- Node.js/npm開発者
- GitHub CLIを使用している方
対象環境
この記事は以下の環境で動作確認しています:
- macOS 15.7.2
- GitHub CLI 2.83.1
- 1Password for Mac 8.x(Shell Plugin機能有効化済み)
この記事ではmacOS固有の設定を使用しています。LinuxやWindowsなど他のプラットフォームを利用する場合は、お使いのOSに応じた設定に読み替えてください。
第1部:攻撃手法の理解
shai-hulud 2.0の攻撃フロー
shai-hulud 2.0は、npmパッケージマネージャーを通じて開発者システムに侵入するマルウェアです。Trend Microの調査レポートによると、開発者の視点から見た攻撃フローは以下の通りです:
-
悪意あるパッケージのインストール:開発者が侵害されたnpmパッケージを
npm install - preinstallフック実行:インストール時にpreinstallスクリプトが自動実行される
- 認証情報の盗み取り:システムに保存されている認証情報(npm publishトークン、GH_TOKENなど)を盗み取る
- GitHubリポジトリへの侵入:攻撃者が取得した認証情報でリポジトリに不正アクセス
- 認証情報の外部送信:盗み取った認証情報を、GitHub Actions経由で攻撃者のサーバーに送信
- 感染拡大:攻撃者が改変したnpmパッケージを再公開し、サイクルが継続
攻撃の特徴
- 自動化された感染拡大:侵害されたパッケージは、preinstallフックにマルウェアを注入して再公開されます
- CI/CD環境の検出:環境変数を確認し、自動化パイプラインか開発者マシンかを判定します
攻撃の影響範囲
盗み取られたGitHub認証情報により、以下の攻撃が可能になります:
- リポジトリへの不正アクセス:プライベートリポジトリのコードを盗み取る
- 署名要件の迂回:GPG署名を無効化し、不正なコードをコミット
- Repository Rulesetsの無効化:Classic PATの場合、署名必須ルールを解除可能
- CI/CD環境への侵入:ワークフロー改ざんによる更なる攻撃
- ファイルシステムの破壊:npmとGitHubのトークン取得に失敗した場合、ホームディレクトリ内のすべてのファイルを破壊
第2部:感染拡大防止策の設定
想定シナリオについて:Trend Microのレポートでは、shai-hulud 2.0がローカルのghコマンドを直接悪用する事例は報告されていません。この記事は、将来的にnpmサプライチェーン攻撃がghコマンドを悪用する可能性を想定した感染拡大防止策を提案しています。
防御戦略の概要
1Password Shell Plugin + Fine-Grained PATは、以下のメカニズムで攻撃を防御します:
- 攻撃スクリプトがghコマンドを実行
- 1Password Shell Pluginが認証要求を発行
-
1Password認証プロンプトが表示
- 人間が拒否 → コマンド実行失敗(攻撃阻止)
- 人間が承認 → Fine-Grained PAT取得
-
Fine-Grained PATの最小権限により制限された操作のみ許可
- Repository Rulesets変更不可
- リポジトリの新規作成・削除不可
- GitHub Actionsワークフロー改ざん不可
防御の要点
第1層:コマンド実行の防御
- 攻撃スクリプトが
ghコマンドを実行しようとすると、1Password認証プロンプトが表示されます - 非対話的環境(攻撃スクリプト)では認証プロンプトを処理できないため、コマンド実行が失敗します
- 人間の承認なしでは、いかなるGitHub操作も実行できません
第2層:権限制限による防御
仮に gh コマンドの実行が許可されても、権限を絞ったFine-Grained PATでは致命的な操作を実行できません:
- リポジトリの新規作成・削除:不可能
- Repository Rulesetsの変更:不可能
- デフォルトブランチへのフォースプッシュ禁止設定を解除できない
- タグの作成禁止設定を解除できない
- ブランチ保護設定を解除できない
- GitHub Actionsワークフローの改ざん:不可能(Workflows権限なし)
- 通常のコード変更・PR作成:可能(開発作業に必要な最小限の権限)
Fine-Grained PATとClassic PATの違い
| 項目 | Classic PAT | Fine-Grained PAT |
|---|---|---|
| 権限の粒度 | 粗い(設定項目が少なく、Read/Write分離不可) | 細かい(Read/Write分離可能) |
| Repository Rulesets変更 | 可能 | Read権限で禁止可能 |
| リポジトリスコープ | 全リポジトリ | 個別指定可能 |
Fine-Grained PATのAdministration: Read権限では、Repository Rulesetsを変更できません。これにより、ブランチ保護ルールを無効化する攻撃を防げます。
設定手順
重要:op plugin init ghコマンドでスコープを設定するまで、Claude Codeなどの非対話型シェル環境ではghコマンドを使用できません。設定作業は通常のターミナル(Terminal.appやVS Codeの通常ターミナル)で行ってください。
詳細な手順は1Password公式ドキュメントを参照してください。
Phase 0: 1Password CLIのインストール(初回のみ)
1Password CLIのopコマンドがすでに利用できる場合は、この手順をスキップしてください。
# インストール確認
op --version
インストールされていない場合は、公式ドキュメントを参照してインストールしてください。
macOSの場合は、Homebrewでインストールできます:
brew install 1password-cli
インストール後、1Password Desktopアプリで「設定 > Developer > Integrate with 1Password CLI」を有効にしてください。
op vault list
インストールに成功した場合、このコマンドで保管庫一覧が表示されます。
Phase 1: Fine-Grained PATの作成
- GitHubのFine-Grained PAT作成画面にアクセス
- 以下の設定で作成:
- Token name:
GitHub-FG-PAT-Daily - Expiration: 30日
- Repository access: All repositories(または個別指定)
- Permissions:
- Contents: Read and write
- Commit statuses: Read only
- Pull requests: Read and write
- Token name:
トークンは再表示できないため、1Passwordへの保存が終わるまでブラウザを閉じないでください。
管理用トークンの作成:GitHub設定変更などの管理作業を行う場合は、別のトークンを作成し、以下の権限を追加することを検討してください:
- Actions: Read only(ワークフロー実行状態の確認)
- Workflows: Read and write(ワークフローファイルの編集)
- Administration: Read only(リポジトリ設定の確認)
Phase 2: トークンを1Passwordに保存
1Passwordブラウザー機能拡張と1Password CLIが接続されている場合、トークンの生成画面で自動的に保存ダイアログが表示されます。表示された場合は、ダイアログの指示にしたがって保存してください。
ダイアログが表示されない場合は、手動で保存が必要です。
- ターミナルで
op plugin init ghコマンドを実行 - Inport into 1Password... を選択
- Enter a name to save it in 1Password:
GitHub-FG-PAT-Daily - 保存する保管庫を選択
Phase 3: GitHub CLI再設定
ブラウザー機能拡張でトークンを保存した場合、以下の手順でトークンを切り替えます。
# 既存のClassic PATからログアウト
gh auth logout --hostname github.com
# 1Password Shell Pluginを初期化
op plugin init gh
# → "Always use this credential" (Global scope) を選択
# → 作成したFine-Grained PATを選択
Phase 4: ヘルパーとプラグインの永続化
gitコマンドで1Password認証を使用するように設定します。
# Git Credential Helperを更新
git config --global --unset-all credential.https://github.com.helper
git config --global --add credential.https://github.com.helper ""
git config --global --add credential.https://github.com.helper \
"!/usr/local/bin/op plugin run -- gh auth git-credential"
次に1Password Shell Pluginを永続化します
# 1Password Shell Pluginの永続化
echo "source ~/.config/op/plugins.sh" >> ~/.bashrc && source ~/.bashrc
このコマンドを設定すると、ターミナル起動時に自動的に1Password Shell Pluginが読み込まれます。設定が完了したら、ターミナルを再起動してください。
Phase 5: GitHub CLI操作の検証
# GitHub CLI操作テスト
gh repo view user/repo --json name,owner
gh pr list --limit 5
各コマンド実行時に1Password認証プロンプトが表示されることを確認してください。
Phase 6: Git操作の検証
# Git操作テスト
git fetch origin
git pull origin main
Git操作でもHTTPS経由のGitHub認証で1Password認証プロンプトが表示されることを確認してください。
1Password Shell Pluginのスコープとは
1Password Shell Pluginの「スコープ」は、認証情報をどの範囲で共有するかを決定する設定です。
スコープの種類
Session scope(セッションごと)
- そのシェルセッションでのみ認証情報を利用可能
- 別のターミナルウィンドウでは利用不可
- セッションごとに対話メッセージでの許諾が必要なので、非対話的環境(Claude CodeのBashツールなど)では動作しない
Directory scope(ディレクトリごと)
- 特定ディレクトリとその子ディレクトリで認証情報を共有
- プロジェクトごとに異なるトークンを使い分け可能
-
.op/plugins/gh.jsonファイルで管理
Global scope(システム全体)
- システム全体で認証情報を共有
- どのディレクトリからでも同じトークンを利用可能
複数のスコープを組み合わせることも可能です。たとえばGlobal scopeを設定しつつ、特定プロジェクトではDirectory scopeを設定すると、ディレクトリスコープが優先して使用されます。
この記事での推奨設定
Global scopeもしくはDirectory scopeを推奨します。
- Claude Codeなどのコーディングエージェントでも
ghコマンドが動作する - 認証プロンプトは1Passwordアプリ(別プロセス)が表示するため、セキュリティは維持される
スコープ設定は認証の頻度を制御しません。認証プロンプトの表示頻度は別の設定(後述)で制御されます。
認証頻度の制御
認証プロンプトの表示頻度は、1Passwordアプリの自動ロック設定で制御されます。
設定場所
1Passwordアプリ > Settings > セキュリティ > セキュリティレビュー
設定オプション
厳格(推奨)
- セキュリティ最優先
- 短期間でロックされ、再認証が必要になる。
バランス
- 1時間ごとの自動ロック
- 端末がスリープすると自動ロック
- 利便性とセキュリティのバランス
高い利便性
- デバイスロック解除後は長時間認証が維持される
- 利便性優先(セキュリティは低下)
この記事での推奨設定
厳格を推奨します。たびたび認証プロンプトが表示されますが、攻撃スクリプトからの不正利用を阻止できます。
第3部:セキュリティ検証結果
検証1: トークン切り替え攻撃への耐性
攻撃シナリオ
攻撃者がシェルセッションを乗っ取り、より高権限なトークンへの切り替えを試みます。
検証コマンドと結果
コマンド1: トークンのクリア
op plugin clear gh --all
結果:
[ERROR] authorization prompt dismissed, please try again
1Password認証プロンプトが表示され、ユーザーが認証を却下するとコマンドが失敗します。
コマンド2: 新規トークンの設定
op plugin init gh
結果:
[ERROR] interactive IO not available
対話的な選択画面が必要なため、非対話的環境では実行できません。
結論
-
op plugin clear gh --allは認証プロンプトを表示し、ユーザーが却下可能 -
op plugin init ghは非対話的環境では実行不可 - 攻撃者はトークンを切り替えたり、クリアできない
検証2: Repository Rulesets変更への耐性
検証コマンド
Fine-Grained PAT(Administration: Read)でRepository Rulesetsの変更を試みます:
# Repository Rulesetsの変更を試みる
gh api --method PUT repos/user/repo/rulesets/12345 \
--field name='Release Tags' \
--field enforcement='disabled'
検証結果
gh: Resource not accessible by personal access token (HTTP 403)
{"message":"Resource not accessible by personal access token"}
結論
- Administration: Read権限ではRulesetsを変更不可
- Classic PATでは変更可能
- 署名必須ルール、ブランチプロテクションルール、タグルールなどを無効化できない
仮に gh コマンドの実行が許可されても、攻撃者はRepository Rulesetsを変更できず、リリースをトリガーするタグの作成も阻止できます。
まとめ
この記事では、npmサプライチェーン攻撃によるGitHub認証情報窃取への対策として、1Password Shell Plugin + GitHub Fine-Grained PATによる感染拡大防止策を紹介しました。
2層の防御メカニズム(認証プロンプトによるコマンド実行制御 + Fine-Grained PATによる権限制限)により、仮にローカル環境が侵害されても、GitHubリポジトリへの感染拡大を阻止できます。
1PasswordによるPAT切り替えの容易性により、最小権限の原則を実践しやすくなり、セキュリティと開発体験の両立が可能になります。
参考文献
- 1Password Shell Pluginが便利
- Shai-hulud 2.0キャンペーンがクラウドと開発者エコシステムを標的に
- 1Password CLI
- Use 1Password to securely authenticate the GitHub CLI
以上、ありがとうございました。