トークン失効の順序を間違えない — Keyv / ChainDrop 侵害時の初動と予防(後編)

sho

この記事は全2回の後編です。
前編: npm install なしで payload が実行される — Keyv / ChainDrop 攻撃の仕組みと検査手順(前編)
後編(この記事): 侵害が疑われたときの初動・失効の順序・予防策

Keyv / ChainDrop 攻撃が2経路で走る仕組みと、チェックアウトせずに設定ファイルを検査する手順は、前編で扱いました。この後編では、侵害が疑われたときに何をどの順序でやるかを扱います。

先に結論を書きます。感染を疑う端末からトークンを失効させてはいけない。 失効そのものを検知して発火するデッドマンスイッチが仕込まれています。

侵害が疑われるときの初動 — 失効の順序を間違えない

この節は順序が結果を左右します。SafeDep が公開したシェルスクリプトには、GitHub API への応答が 40x(トークン失効を意味する) になったことを引き金に、変数 HANDLER の内容を eval する構造が含まれました。

永続化に使われた unit 名は次の2つです。

  • macOS: ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
  • Linux: ~/.config/systemd/user/gh-token-monitor.service

Linux では linger を有効化してログアウト後も生存し、24時間ハンドラが発火しなければ自動停止します(SafeDep と StepSecurity が一致)。Wiz は、マシン固有の SHA256 フィンガープリントを送信して標的ごとにデッドマンスイッチを配信する挙動を報告しています。

handler の中身は不明だ。 SafeDep は今回の handler の内容は不明であり、任意のローカル実行として扱うべきだと明記しています。過去の類似事例で rm -rf ~/ が使われたという注記はありますが、今回がそれだと断定する根拠はありません。「何が起きるか分からないローカル実行」と見積もります。したがって順序はこうなります。

  1. 再実行を止める — install・release・deploy を停止し、IDE でのリポジトリ再オープンも止める
  2. 隔離する — 端末やランナーの外部通信を遮断する(電源断は証拠保全の方針を先に確認する)
  3. watcher の path を確認して保全する — 上記の plist / service ファイルの存在と内容を記録する
  4. 無力化する — サービス停止と linger 無効化
  5. クリーンな別端末から資格情報を失効させる
  6. known-good な環境で再構築する

並行して担当(インシデントリード、証拠、GitHub/npm、クラウド)を立てます。台帳に秘密の値は記録せず、存在・種類・owner・scope だけを書きます。

失効の範囲は権限境界で決める

失効の対象は、見つかったファイルの中身ではなく「実行時に到達可能だった秘密情報」という権限境界で決めます。おおむね4カテゴリです。

  • SOURCE: GitHub PAT / App / SSH、npm token / OIDC、release 権限
  • CLOUD: AWS / GCP / Azure、Secrets Manager / SSM、service account
  • RUNTIME: Kubernetes / kubeconfig、Vault、DB 接続情報、Terraform
  • DEVELOPER: SSH 鍵、.env、Claude / Codex / Cursor の設定、API キー

API キーの失効手順は提供元ごとに違います。Anthropic は Claude Console の Settings → API Keys から該当キーを Delete して再発行します。OpenAI は platform.openai.com/api-keys から削除しますが、この削除は取り消せない

復旧は掃除ではない

node_modules と lockfile の削除、npm cache clear、同じ端末でのクリーンインストールは、いずれも単独では不十分です。token watcher、IDE hook、盗まれた資格情報、二次改ざんが残ります。known-good なイメージ・コミット・lockfile を選定し、旧環境を隔離して、スクリプト拒否の状態で依存を取得します。端末・ランナーの信頼回復と、資格情報の信頼回復は別の作業だ。

判定を二択にしない — 証拠の5段階と4面調査

「安全か、侵害されたか」の二択で答えを出そうとすると、調べていない範囲が「安全」に吸収されます。判定は5段階で書きます。

  1. confirmed-compromise — ハッシュ一致・実行・永続化・外部送信が確認された
  2. probable-exposure — 侵害バージョンの実行が濃厚だが痕跡は未確認
  3. affected-artifact-only — lockfile / cache / artifact にのみ確認された
  4. no-evidence-found — 指定した範囲では痕跡なし
  5. not-assessed — 端末・CI・ログ・権限を未確認

4番は「検査した範囲と時点にだけ有効」な判定であり、5番と混ぜてはいけません。no-evidence-found と書くときは、対象期間・ログソース・保持期間・未確認範囲を併記します。

調査の面も分け、面ごとに判定・owner・未確認事項・次の証拠を残します。

  1. REPOSITORY — lockfile / package.json / 全ブランチ / .claude / .vscode / workflow
  2. ENDPOINTnode_modules / cache / プロセス / LaunchAgent / systemd / tmp
  3. CI/CD — run / job / runner / artifact / メモリ読み出し / Bun ダウンロード / egress
  4. CONTROL PLANE — GitHub / npm / クラウド / Kubernetes / Vault の publish・access・secret 監査

予防策と、それぞれが守れない範囲

npm v12 の install script 既定拒否

npm v12.0.0 は2026年7月8日にリリースされました。lifecycle scripts(preinstall / install / postinstall、非レジストリ source 向けの preparebinding.gyp による暗黙の node-gyp 再ビルド)が既定でブロックされ、allowScripts にエントリのない依存はスクリプトを無音でスキップします。.npmrc のキーは allow-git / allow-remote、既定値は none、取り得る値は all / none / rootroot はプロジェクトの package.json 直下で宣言されたもののみ)。

導入は3段構えになります。

# 1. 可視化(読み取り専用。package.json を変更しない)
npm approve-scripts --allow-scripts-pending
# 2. レビュー:source・publisher・version・script の内容・そもそも必要かを確認
# 3. 限定許可(package@version 単位で package.json に記録される)
npm approve-scripts  [ ...]
npm approve-scripts --all

公式ドキュメントに literal な JSON の例は掲載されていないので、allowScripts は手書きせず npm approve-scriptspackage.json を書かせます。

守れない範囲が3つあります。 npm approve-scripts は workspace 非対応(公式に “Note: This command is unaware of workspaces.” と記載)。そして global install と npx は管理外です。

“Running it with --global (-g) fails with an EGLOBAL error, since global installs (npm install -g) and one-off executions (npm exec / npx) have no project package.json to write to.”
--global-g)付きで実行すると EGLOBAL エラーになる。global install(npm install -g)と単発実行(npm exec / npx)には書き込み先のプロジェクト package.json がないためである)

CI・global・npx・別のパッケージマネージャは別に管理します。

Trusted Publishing と Staged Publishing

Trusted Publishing は OIDC で npm registry と CI/CD の間に短命・署名付きトークンの信頼関係を作り、長期の publish token を不要にします。

対応プロバイダは GitHub Actions(GitHub-hosted runner)/ GitLab CI/CD(GitLab.com shared runner)/ CircleCI(CircleCI cloud)の3つで、self-hosted runner は未対応(将来対応予定)。self-hosted のパイプラインは長期トークンを持つので、有効期限と scope の管理を別に設計します。

Staged Publishing で許可アクションを npm stage publish に限定すると、CI は stage までしか行えず、人間が 2FA で承認してから一般公開されます。ただし今回のキャンペーンが示したように、OIDC はトークン流出のリスクを下げるだけで、悪意あるソースがビルドされることは止めない。

GitHub 側

Branch protection rule は1ブランチにつき1ルールしか同時適用できない制約があり、GitHub は Ruleset の利用を推奨しています。PR レビュー、status check、直 push 制限、bypass の最小化を Ruleset 側で組みます。

また、パッケージマネージャのアカウントに passkey 相当の phishing-resistant MFA を有効化し、長期トークンに有効期限を設定します。CI 依存のバージョンは pin します。

既存の制御が守れる範囲

制御有効なこと残る穴
lockfile正確な version と integrity の証拠侵害 version を固定してしまう / lockfile 外の install は守れない
npm audit / SCA既知の脆弱性・malware alert反映遅延・変種・対象外の実行面
OIDC / provenance公開主体・workflow・source の出所悪意ある source を正規 pipeline がビルドできる
Verified commit署名や GitHub 経由の作成盗まれた権限・API commit の意図までは保証しない

単一の制御で閉じる組み合わせはありません。スクリプト拒否 × version の証拠 × source 差分 × egress 制御 × 最小権限を重ねます。

まとめ

侵害が疑われた時点で守るべき順序を4点に絞ります。

  1. 失効を最後に回します。 再実行停止 → 隔離 → watcher の path 保全 → 無力化 → クリーンな別端末からの失効 → known-good 環境での再構築
  2. 失効範囲は権限境界で決めます。 ファイルの中身ではなく、実行時に到達可能だった SOURCE / CLOUD / RUNTIME / DEVELOPER の秘密情報を対象にします
  3. 判定を二択にしません。 no-evidence-found は検査した範囲と時点にだけ有効で、not-assessed と混ぜません
  4. 予防策は守れない範囲まで書いて採用します。 npm v12 の allowScripts は workspace / global / npx を管理できず、Trusted Publishing は self-hosted runner に未対応で、いずれもソースの侵害は止めません

FAQ

侵害が疑われる端末の GitHub token はすぐ失効させるべきですか

失効させるべきですが、その端末からは操作しません。SafeDep が公開したシェルスクリプトには、GitHub API への応答が 40x(トークン失効)になったことを引き金に handler を eval するデッドマンスイッチの構造が含まれました。handler の中身は不明で、任意のローカル実行として扱うべきだと SafeDep は明記しています。隔離 → watcher の保全 → 無力化 → 別端末からの失効 → known-good 環境での再構築の順序を守ります。

npm v12 の allowScripts を有効にすれば install script 経由の攻撃は防げますか

プロジェクト内の依存インストールは lifecycle scripts が既定でブロックされます。ただし npm approve-scripts は workspace 非対応で、global install(npm install -g)と単発実行(npm exec / npx)は書き込み先の package.json を持たないため対象外になります(--global 付きの実行は EGLOBAL エラーになる)。さらに Keyv / ChainDrop のソース経路は install script を通らないので、allowScripts だけでは止まりません。

調査報告に no-evidence-found と書いてよいのはどんなときですか

指定した検査範囲で痕跡が見つからなかったときだけです。端末・CI・ログ・権限のいずれかを未確認のまま「痕跡なし」と書くと、実態は not-assessed になります。no-evidence-found は検査した範囲と時点にだけ有効な判定なので、対象期間・ログソース・保持期間・未確認範囲を併記します。


参考情報

カテゴリー: 業界最新情報