
お好きな金額でサポートしていただけると嬉しいです!
(50円〜10,000円[税込]まで)
作成日時:2026年5月16日 01:12
更新日時:2026年5月19日 00:15
セキュリティ
サプライチェーン
2026年5月11日、TanStackの42個のnpmパッケージに84個の悪意あるバージョンが公開されました。脅威グループTeamPCPによる「Mini Shai-Hulud」キャンペーンの一部で、3つのGitHub Actions脆弱性をチェーン化して実行された攻撃です。
この事件は「npmトークンを盗まなかった」という点で従来の手口と異なり、GitHub OIDC信頼発行者の仕組みを悪用して実行時に認証情報を抽出した点が新しいものでした。
続々と続くサプライチェーン攻撃について、もう少し仕組みと対策を整理したくてまとめてみました。
※あくまで自分の理解のためにまとめた記事執筆時点での情報です。個人・組織に関わらず適切な対応ルール等があるはずですので、本執筆内容はあくまで対策等を考えていくうえでの参考に留めていただくようお願いいたします。
まずはトリガー別の実行環境と、Secretsへのアクセス、リスクレベルをまとめるとこんな感じ。
トリガー | 実行環境 | Secretsへのアクセス | リスクレベル |
|---|---|---|---|
| ベースリポジトリ | あり | 低(管理者のみトリガー) |
| フォークコンテキスト | なし(フォークPR) | 低(secrets隔離) |
| ベースリポジトリ | あり(フォークPRでも) | 高(今回問題となった部分) |
| ベースリポジトリ | あり | 中(適切に分離) |
このうち今回問題となったpull_request_target についてですが、これは「secretsさえ渡さなければ安全」という話ではありません。このトリガーは外部(フォーク)の人がトリガーできる一方で実行環境がベースリポジトリ側にある、という部分が危険とされる所以です。以下の性質を持つことになります。
GITHUB_TOKEN の権限(設定次第で書き込みが有効に。最小権限徹底。神キー絶対ダメ)そのため、pull_request_target が許容される用途は、PRのメタデータのみ扱い、フォークのコードをチェックアウトして実行しない以下のような場合に限る、とされています。
重要: GitHub Actionsキャッシュはリポジトリ内の全トリガータイプで共有されます。permissions: フィールドで読み取り専用を設定しても、actions/cache の事後保存ステップはランナー内部のトークンを使用するため、キャッシュ書き込みはブロックできません。
pull_request_target → push → release ワークフロー、すべてが同じキャッシュを共有する${{ github.ref }} やリポジトリ所有者情報で信頼境界を明示する必要があるGitHub ActionsはOIDCを使って短命な資格情報を生成できます。id-token: write権限を持つジョブは、npmやプロバイダーに対してパスワードレスで認証できます。これにより、
/proc/<pid>/mem 経由で抽出可能という特性があります。
各脆弱性は既知のもので、単独では機能しない状態にありました。次の脆弱性が想定していた信頼境界を橋渡しすることでのみ攻撃が成立しています。
pull_request_target をトリガーするPRを開く(ベースリポジトリの実行環境を与えられる)bundle-size.yml がこのトリガーで actions/cache@v5 を使用していた(依存物などをキャッシュに保存・復元する)補足: GitHub Actionsのpermissionsは基本的にワークフローに発行されるGITHUB_TOKENがGitHub APIに対して何を実行できるか、という部分の権限を絞るための仕組みです。しかし、GitHub Actionsのキャッシュは、GitHub APIに対する権限(permissions)と別系統の権限・経路で動くもので、ベースリポジトリの実行環境が与えられている状態だと、ベースリポジトリのキャッシュ領域に対してsaveが行えてしまいます。
release.yml)は id-token: write 権限を持っている状態(OIDCトークンを取得する権限を持つ)registry.npmjs.org に直接 POST → ワークフローの発行ステップをバイパスされてしまったname: Vulnerable Build
on:
pull_request_target:
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # フォーク側コードをチェックアウトしてる
- run: npm ci # メインリポジトリの実行環境でフォーク側のコードが実行されてしまう
- run: npm run build
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # 危険:フォーク側が読み取り可能
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} # これも同じく
問題点:
pull_request_targetを使用しながらフォーク側のコードをチェックアウト・実行しているNPM_TOKEN、AWS_ACCESS_KEY_ID)が、外部コントリビューターのコードから読み取り可能な環境に存在uses: actions/some-action@v4 や @main は任意のコードに差し替えられる恐れがあるpermissions: write-all やデフォルトのまま放置(GitHubのデフォルト権限はリポジトリの設定により異なるが、contents: write になっている場合もあるため最小権限を明示的に設定することが重要)run: に渡すとスクリプトインジェクションを招くでは、対策としてどのような設定とすべきなのでしょうか。今回の反省部分を踏まえて調べてみました。
# .github/workflows/build.yml
# 信頼できない処理(ビルド):pull_request トリガーで実行
name: Build PR
on:
pull_request:
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@<sha> # v6.x.x
- run: npm ci
- run: npm run build
- name: Save PR number
run: echo ${{ github.event.number }} > ./pr_number
- uses: actions/upload-artifact@<sha> #v7.x.x
with:
name: pr_artifacts
path: ./pr_number
# .github/workflows/comment.yml
# 信頼された処理(書き込み):workflow_run で実行
name: Comment on PR
on:
workflow_run:
workflows: ["Build PR"]
types:
- completed
jobs:
comment:
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
permissions:
pull-requests: write
steps:
- uses: actions/download-artifact@<sha> # v7.x.x
with:
name: pr_artifacts
- name: Comment
run: |
PR_NUMBER=$(cat pr_number)
gh pr comment $PR_NUMBER -b "Build succeeded"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ポイント: pull_request でフォークコードを実行し結果を artifact で保存。workflow_run で信頼された書き込み処理を実行。両ワークフローで権限を分離できています。
name: Secure Release
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@<sha> #v6.x.x
- uses: actions/setup-node@<sha> #v6.x.x
- run: npm ci
- run: npm test
publish:
needs: test
if: github.repository == 'YourOrg/your-repo' # リポジトリオーナーガード
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # 発行ジョブのみに限定
steps:
- uses: actions/checkout@<sha> # v6.x.x
with:
ref: refs/heads/main
# キャッシュを使用しない(安全優先)
- uses: actions/setup-node@<sha> #v6.x.x
- run: npm ci
- run: npm run build
- name: Publish to npm
env:
NPM_CONFIG_PROVENANCE: true
run: npm publish
{
"trusted_publishers": [
{
"type": "github_actions",
"repository": "YourOrg/your-repo",
"workflow": ".github/workflows/release.yml",
"branch": "refs/heads/main"
}
]
}pull_request_target を避け、可能な限り pull_request を使用するpull_request_target を使う場合は、フォーク側コードを絶対にチェックアウト・実行しないcontents: read に設定し、必要なジョブのみ昇格id-token: write は発行ジョブのみに限定actions/checkout@<sha>)if: github.repository == 'YourOrg/your-repo' でリポジトリオーナーガードを追加${{ github.ref }} や信頼境界情報を含めるpull_request_target ワークフローで actions/cache を使用しないrun: で直接文字列展開しない。action パラメータとして渡すTanStack の42パッケージすべてが有効な SLSA Build Level 3 provenance(Sigstore attestation)を持っていました。それでも攻撃は成功しています。
TanStack/router の release.yml@refs/heads/main で構築された」行動分析と組み合わせることが必須。自動行動分析は6分以内に84個すべての異常(router_init.js の難読化パターン)を検出した。これがなければどうなってたか、考えるだけでも恐ろしいですね。感謝です。
npmの「dependents が存在する場合は unpublish しない」ポリシーにより、影響を受けたほぼすべてのパッケージでunpublishが利用不可でした。npm security がサーバー側でtarballを削除する必要があり、その間悪意のあるtarballはインストール可能なまま残りました。
今回の事件で使われたペイロードは60秒ごとにGitHubをポーリングする常駐デーモンをPCにインストールするものでした。40X エラー(トークン失効)という条件を検出すると rm -rf ~/ を実行する動きを取ります。インシデント対応時は必ず存在確認・削除の確認を行ってください。
~/Library/LaunchAgents/com.user.gh-token-monitor.plist~/.config/systemd/user/gh-token-monitor.servicepackage-lock.json / pnpm-lock.yamlなど)でバージョン検査gh-token-monitor サービスを削除.claude/・.vscode/ などの配下の router_runtime.js・vite_setup.mjs を検索・削除(npm uninstall では残存する)「既知の脆弱性を組み合わせる」という今回の手口は、単体では防げていたものが連鎖して突破されるという驚きと恐ろしさ、対策の難しさを改めて実感しました。
特にGitHub Actionsキャッシュがすべてのトリガータイプで共有されるという挙動は、権限設定では防ぎきれない点として頭に刻んでおきたいところです。
正解はまだまだ全然わかりませんが、いざという時のためにできることはしておきたいですね。もっと良い方法があるよ!などあればぜひ情報交換させていただけると嬉しいです!
それでは、また。
HTNCode