この記事について
2025〜2026 年にかけて、npm と GitHub Actions を狙うサプライチェーン攻撃が立て続けに起きました。tj-actions、Shai-Hulud、Trivy/Aqua、@antv、mastra……。
これらへの対策は、ひとつひとつは数行の設定です。Action を commit SHA で pin する、install スクリプトを無効化する、cloud 認証を OIDC にする——地味で、後回しにしやすい。
ですが「実際に起きた攻撃から逆算」すると、どれがなぜ要るのか・どれが本当に効いたのかが見えてきます。この記事は、侵入経路ごとに「公開インシデント → 効く防御」を対応づけるものです。
数値・日付は公開ソースに基づきますが、解析元によって食い違うものは幅を持たせるか、ぼかしています。末尾に出典をまとめています。
先に結論
攻撃面ごとに効く防御をまとめると、やることは次の8つです。
- GitHub Action は commit SHA で pin する
- Action が入れる tool は version を固定し、Docker image は digest で pin する
- workflow permissions を最小化し、クラウド認証は OIDC に切り替える
- install スクリプトを実行しない(
ignore-scripts等) - 依存に cooldown(最小リリース経過時間)を設定する
-
lockfile 通りに install する(
npm ci/--frozen-lockfile等) - Dependabot malware alertsを有効化する(すり抜けた後の最後の網)
-
npx/pnpm dlxで実行時に取得する参照も固定する(lockfile にも cooldown にも乗らないため)
具体策は8つですが、本質的には最後に挙げる「5つの型」に集約できます。これらが「なぜ要るのか」を、実際に起きた攻撃から順に見ていきます。
1. GitHub Actions を狙う
1-1. Action 自体が書き換えられる → commit SHA で pin する
インシデント:tj-actions/changed-files(2025年3月、CVE-2025-30066)
広く使われていた GitHub Action tj-actions/changed-files が侵害されました。攻撃者は多数のバージョンタグ(v1〜v45 系)を、ひとつの悪性コミットを指すよう付け替えました。タグで参照していた利用者は、いつも通りのつもりで一斉に悪性コードを引き込みました。
悪性ペイロードは CI 環境内の secret を集め、ワークフローの(公開)ログに出力しました。外部サーバーへ送信したわけではなく、public リポジトリではログが誰でも見られるため露出した、という経路です。
被害規模は誇張されがちですが、正確には「この Action を使っていたリポジトリが 23,000 超」であって、それが被害数ではありません。実際に secret を公開ログへ漏らしたのは、調査で約 218 リポジトリと報告されています(到達面と実害は分けて理解すべきです)。
効く防御:Action を可変タグではなく commit SHA で pin する
# タグ参照(可変。指す先を書き換えられる)
- uses: tj-actions/changed-files@v45
# commit SHA 参照(不変)
- uses: tj-actions/changed-files@<full-40-char-commit-sha>
タグは可変、commit SHA は不変です。あるベンダ解析は「hash で pin していた利用者は影響を受けなかった(露出期間中に悪性 hash へ更新していない限り)」と明言しています。pinact や zizmor のようなツールで「全 uses: が SHA pin されているか」を CI で強制できます。
1-2. 流出した認証情報の影響範囲 → 権限の最小化 + OIDC
同じ tj-actions の件で示唆的なのは、漏れた secret の大半が GITHUB_TOKEN だった点です。これはジョブ終了時(最大でも 24 時間)で失効するため、攻撃者にとって価値が低い。逆に、CI 内で到達可能だった 長寿命の static credential(DockerHub / npm / クラウドのアクセスキー等)こそが高リスクでした。
効く防御:
-
workflow permissions を最小化する(
permissions:を明示し、必要な read だけに絞る)。 - クラウド認証を OIDC に切り替える。長寿命の static key を CI に置く代わりに、OIDC で発行される短寿命トークンを使う。漏れても悪用可能な時間窓を大幅に短縮できる(有効期限はクラウドや設定で変わるが、長寿命の static key よりはるかに短い)。何かが走ってしまったときの爆発半径を詰める二段目として効きます。
1-3. Action が入れる tool / image → version を固定し、image は digest で pin
インシデント:Trivy / Aqua Security(2026年3月、CVE-2026-33634)
リリース用の認証情報が侵害され、悪性のビルドが公開されました。Docker image は 0.69.4 だけでなく 0.69.5 / 0.69.6、そして latest タグまでもが悪性化(最後の正常版は 0.69.3)。関連する trivy-action / setup-trivy のタグも悪性コミットを指すよう書き換えられました。
ここでの最重要の教訓はこれです:
Action 自体を SHA pin していても、その Action が install する tool のバージョンを
latest指定していると、新しく公開された悪性リリースを取り込んでしまう。
実際、ベンダの公式 discussion で「version: latest を明示指定していた利用者が、露出期間中に悪性バイナリを取得して被害を受けた」と確認されています。攻撃面は2つあったわけです——Action のコード自体(commit SHA pin で防げる)と、Action が落としてくる tool(version 指定が latest だと防げない)。
図の通り、commit SHA pin は Action 自体の改ざんを防ぐだけで、Action が後からダウンロードする成果物(tool / image)までは固定しません。ここは取り違えやすいポイントです。
効く防御:
- Action が入れる tool の バージョンを固定する(
latestを避ける)。 -
Docker image は tag ではなく
@sha256:の digest で pin する。Docker 公式が「可変タグはセキュリティ境界ではない」と明言しており、digest 参照していた利用者はこの件で影響を受けていません。
# tag(可変。上書きされうる)
FROM aquasec/trivy:0.69.4
# digest(不変)
FROM aquasec/trivy@sha256:<digest>
2. JavaScript パッケージマネージャの install を狙う
2-1. install スクリプトでコードが走る → スクリプトを無効化する
インシデント:Shai-Hulud worm(2025年)
npm エコシステム初の自己伝播型ワームです。第1波(2025年9月、@ctrl/tinycolor などが起点、500 以上のパッケージ)では、インストール時に走るスクリプトが環境内の認証情報を収集して外部へ送り出し、さらに盗んだ認証情報を使って他のパッケージにも自分を publish することで自己増殖しました。第2波("2.0 / The Second Coming"、2025年11月21〜23日)はさらに大規模で、796 のパッケージと 25,000 以上のリポジトリに及びました。
肝は、preinstall / postinstall といったライフサイクルスクリプトが、install するだけで実行される点です。
効く防御:install スクリプトを実行しない
| パッケージマネージャ | 設定 |
|---|---|
| pnpm (v10+) | postinstall はデフォルトで無効(2025年の v10 で変更)。信頼するものだけ allowBuilds に明示 |
| npm |
.npmrc に ignore-scripts=true
|
| yarn berry (v2+) |
.yarnrc.yml に enableScripts: false
|
| bun |
bunfig.toml の [install] に ignoreScripts = true
|
「以前は安全だったパッケージが、あるバージョンから急に悪性スクリプトを持つ」攻撃に対し、デフォルトで実行しない・allowlist 方式にしておくのが効きます。
本記事で挙げる設定キー名やデフォルト値は、ツールのバージョンによって変わります。導入時は各公式ドキュメントで確認してください。
2-2. 公開直後を突かれる → cooldown(最小リリース経過時間)
悪性バージョンは、 公開から検知・削除までが短時間(数時間〜数日) であることが多い、と複数の分析が報告しています。裏を返せば、新しく公開されたバージョンをしばらく入れないだけで、悪性の窓の多くを避けられます。
効く防御:cooldown を設定する
| ツール | 設定 |
|---|---|
| pnpm |
minimumReleaseAge(v11 のデフォルトは 1440 分 = 1 日。10080 = 1 週間も可) |
| Renovate | minimumReleaseAge |
| Dependabot | cooldown |
| npm | min-release-age |
cooldown 期間をどれだけ取るかは方針次第ですが、たとえば「Dependency cooldowns」を提唱する Christian Schneider は 7 日を推奨しています。1 日でも「検知が早い大半の攻撃」はかなり避けられる、というのが各ツールのデフォルト値の考え方です。
2-3. install が依存を再解決する → lockfile 通りにインストールする
ワームは新しい悪性バージョンを高速に公開します。ここで誤解しやすいのが、lockfile があっても npm install は状況次第で依存を解決し直す点です(package.json の範囲指定や lockfile とのズレに応じて新しいバージョンに更新し、lockfile 自体を書き換えることもある)。その結果、lockfile に無い新しい(=悪性の)バージョンを引き込み得ます。
効く防御:lockfile を commit し、再解決しない install を使う
npm ci # package-lock.json の通りに入れる
pnpm install --frozen-lockfile # lockfile と差異があれば失敗
yarn install --immutable # 同上(yarn berry)
CI と本番では、lockfile に固定されたバージョン通りに入る install を使うのが基本です。
2-4. 既知の悪性バージョンを事後に検知する → malware アラート(最後の網)
インシデント:@antv エコシステム(2026年5月19日、"Mini Shai-Hulud")
メンテナのアカウント乗っ取り経由で、約 640 version / 323 パッケージに悪性版が公開されました(@antv を中心に、同じメンテナの他パッケージも巻き込み)。preinstall に仕込まれたスクリプトが CI / クラウドの認証情報を窃取しました。
cooldown をすり抜けて入ってしまった後でも気づける最後の網があります。悪意あるバージョンは順次 GHSA に malware advisory として登録されます。
効く防御:Dependabot malware alertsを有効にする
GitHub は 2026 年 3 月に「Dependabot malware alerts」というオプトインを追加しました。これを有効にすると 既存の依存にも遡って 適用され、lockfile に既に入っている既知の悪性バージョンも事後通知されます。
注意:これは通常の Dependabot security alert とは別のトグルです。malware advisory は従来アラート対象外だったため、この Dependabot malware alerts を明示的に有効化する必要があります。
2-5. install を介さず実行時に取り込まれる → 実行時参照も pin する
ここまでは「install するときに何が起きるか」でした。ですが npx <pkg>@latest(CI、Makefile、devcontainer、AI エージェントの MCP サーバ設定など)、pnpm dlx、Dockerfile の image:latest のように、install を経ず実行のたびに最新を取りに行く経路が増えています。これらはマニフェストにも lockfile にも現れないため、Renovate / Dependabot は既定で追従せず、cooldown(2-2)も lockfile(2-3)も効きません。
インシデント:mastra npm scope の乗っ取り(2026年6月)
元コントリビュータの npm アカウント乗っ取りを起点に、@mastra/*(パッケージ数 141 前後)へ悪性依存が注入され postinstall で RAT が起動しました——ここまでは 2-1 の型です。注目したいのは別の面で、よく npx -y @mastra/mcp-docs-server(版指定なし=@latest)で起動される MCP サーバの latest タグが、1 日のうちに 1.1.46(健全)→ 1.1.47(侵害版・後に削除)→ 1.1.48(健全) と書き換わった点です。版を固定せず起動していれば、汚染版が latest を指す数時間に当たった瞬間、lockfile も cooldown も無関係に実行時に掴みえたわけです。
効く防御:実行時参照を pin し、固定したものを cooldown 付きで追従させる
npx pkg@latest → npx pkg@1.2.3、image:latest → image@sha256:<digest> と固定します。ただし pin できるのは直接参照だけで、間接依存は実行時に semver 解決されたまま(前述の axios がこれ)。完全に固めるなら依存ごとバンドルして lockfile を commit するのが本筋で、pin は「窓を狭める緩和策」です。
固定したきり古くならないよう、Renovate の custom manager(regex) で実行時参照を拾えば、通常依存と同じ cooldown 付きで更新 PR が出せます(npm datasource はリリース時刻を持つので minimumReleaseAge がそのまま効く)。対象ファイルは MCP クライアントにより異なる(.mcp.json / .cursor/mcp.json 等)ので、末尾が mcp.json のものをまとめて拾います。なお現行キーは managerFilePatterns(旧 fileMatch は deprecated):
{
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/mcp\\.json$/"],
"matchStrings": ["\"(?<depName>@?[\\w./-]+)@(?<currentValue>\\d+\\.\\d+\\.\\d+[\\w.-]*)\""],
"datasourceTemplate": "npm"
}
]
}
これは新しい型ではなく、型①(不変参照に固定)と型③(時間を稼ぐ)を「実行時の入口」にも当てる、という話です。
2025–2026 の教訓は「5つの型」に集約できる
攻撃面ごとに見ていくと、効いた防御は次の型に分類できます。
- 不変参照に固定する — Action は commit SHA、Docker image は digest。可変タグは書き換えられる。
-
実行を止める — install スクリプトを実行しない(
ignore-scripts等)。「入れる」ことと「実行する」ことを分ける。 -
時間を稼ぐ — cooldown /
minimumReleaseAge。悪性バージョンは早期に検知・削除されることが多い。 - 巻き込み範囲を詰める — workflow permissions の最小化、長寿命の credential を OIDC の短寿命トークンに。漏れたときの爆発半径を小さく。
- 事後に気づく — lockfile 通りの install + malware アラート。すり抜けた後の最後の網。
| 攻撃面 | 公開インシデント | 効く防御(型) |
|---|---|---|
| Action 改ざん | tj-actions/changed-files (2025/3) | commit SHA pin(①) |
| 流出 credential の悪用 | tj-actions(同上) | permissions 最小化 / OIDC(④) |
| tool / image の取り込み | Trivy/Aqua (2026/3) | version 固定 / image digest pin(①) |
| install スクリプト実行 | Shai-Hulud (2025/9・11) | ignore-scripts(②) |
| 公開直後の取り込み | Shai-Hulud(同上) | cooldown(③) |
| 依存の再解決 | Shai-Hulud(同上) | lockfile 通りに install(⑤) |
| 既知の悪性バージョンの残留 | @antv (2026/5) | Dependabot malware alerts(⑤) |
実行時取得(npx / :latest) |
mastra / DataDog Action (2026/6) | 参照を pin(①)+ cooldown 追従(③) |
どれも数行の設定です。地味ですが、2025〜2026 の実例が示すのは、これらが「あれば防げた / 被害を縮小できた」場面が現実にあった、ということです。
出典
tj-actions/changed-files
- Wiz — GitHub Action tj-actions/changed-files supply chain attack (CVE-2025-30066): https://www.wiz.io/blog/github-action-tj-actions-changed-files-supply-chain-attack-cve-2025-30066
- StepSecurity — Harden-Runner detection: https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised
- Endor Labs — Blast radius(実害 218 repos の分解): https://www.endorlabs.com/learn/blast-radius-of-the-tj-actions-changed-files-supply-chain-attack
- GitHub Advisory GHSA-mrrh-fwg8-r2c3 / CISA alert (2025-03-18)
- GitHub OIDC(短寿命トークン): https://github.blog/security/supply-chain-security/secure-deployments-openid-connect-github-actions-generally-available/
Trivy / Aqua Security
- Docker 公式 — Trivy supply chain compromise(digest pin / image 一覧): https://www.docker.com/blog/trivy-supply-chain-compromise-what-docker-hub-users-should-know/
- Aqua 公式 discussion #10425(
version: latest被害の一次ソース): https://github.com/aquasecurity/trivy/discussions/10425 - GHSA-69fq-xp46-6x23 / StepSecurity 解析
Shai-Hulud
- CISA — Widespread Supply Chain Compromise Impacting npm Ecosystem (2025-09-23): https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem
- Wiz — Shai-Hulud npm Supply Chain Attack: https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack
- Check Point / Microsoft / Datadog — Shai-Hulud 2.0("The Second Coming", 2025/11)
- pnpm — Supply chain security(ignore-scripts / minimumReleaseAge): https://pnpm.io/supply-chain-security
cooldown
- Christian Schneider — Dependency cooldowns: https://christian-schneider.net/blog/dependency-cooldowns-supply-chain-defense/
@antv(Mini Shai-Hulud)
- Microsoft Security Blog (2026-05-20): https://www.microsoft.com/en-us/security/blog/2026/05/20/mini-shai-hulud-compromised-antv-npm-packages-enable-ci-cd-credential-theft/
- StepSecurity / Snyk 解析
- GitHub Changelog — Dependabot npm malware alerting (2026-03-17): https://github.blog/changelog/2026-03-17-dependabot-now-detects-malware-in-npm-dependencies/
mastra(npm scope 乗っ取り、2026年6月)
- Socket: https://socket.dev/blog/mastra-npm-packages-compromised
- SafeDep: https://safedep.io/mastra-npm-scope-takeover-supply-chain-attack/
- StepSecurity: https://www.stepsecurity.io/blog/mastra-npm-packages-compromised-using-easy-day-js
- Mend: https://www.mend.io/blog/mastra-npm-scope-takeover-easy-day-js/
- GHSA malware advisory: GHSA-pp62-grrw-hvfp(@mastra/core)/ GHSA-rjjh-j9jr-wg46(@mastra/deployer)
- mastra 公式 issue: https://github.com/mastra-ai/mastra/issues/18045
- Flatt Security 解析: https://blog.flatt.tech/entry/mastra_compromise
実行時取得(npx / dlx)
- DataDog —
npx @latestランタイム侵害事例(issue #49): https://github.com/DataDog/junit-upload-github-action/issues/49 - npm CLI —
npx: https://docs.npmjs.com/cli/v11/commands/npx/ /npm cache: https://docs.npmjs.com/cli/v11/commands/npm-cache/ - pnpm —
dlx: https://pnpm.io/cli/dlx - Renovate —
minimumReleaseAge: https://docs.renovatebot.com/key-concepts/minimum-release-age/ / custom regex manager: https://docs.renovatebot.com/modules/manager/regex/ - Docker — build best practices(タグは mutable / digest pin): https://docs.docker.com/build/building/best-practices/