2026年3月OSS のサプライチェーン事故が立て続けに起きました。

  • Trivy では攻撃者が trivy-action の 77 個中 76 個の version tag と setup-trivy の 7 個の tag を malicious commit に向け直し、v0.69.4 の不正バイナリ公開。
  • LiteLLM では 1.82.71.82.8 が不正に PyPI に載り、公式は後続の 1.83.0 を clean version として案内。
  • axios では 1.14.10.30.4 が短時間 npm に公開され、隠し依存 plain-crypto-jspostinstall からクロスプラットフォーム RAT (感染した端末を遠隔操作できるマルウェア)の配布。(Aqua)

このような事例を防ぐためによくおすすめされるのが npmのmin-release-age や pnpmのminimumReleaseAgeです。 npm の min-release-age は公開から指定日数より新しい版を入れない設定で、pnpm の minimumReleaseAge は同じ考え方を分単位で提供しています。 どちらも「公開直後の毒入り版をすぐ拾う」事故を減らすにはとても有効ですが、守れるのは依存を解決する瞬間までで、install script の自動実行、mutable tag を参照する CI、長期 publish token の残存までは止められません。pnpm 自身も、compromised package(改ざんされたパッケージ) は早期検知されることが多い一方で、公開から検知までにはどうしても露出ウィンドウがある、と整理しています。(npm Docs)

このスクショが象徴的でした。pnpm の stable バージョン では blockExoticSubdepsstrictDepBuilds はどちらもデフォルト false だが、next docs と v11 の release notes ではどちらも true 側に変わっています。しかも blockExoticSubdeps は間接依存が git や tarball URL のような exotic source を引くことを防ぎ、strictDepBuilds は未レビューの build script があれば install を失敗させることができます。 pnpm はセキュリティ重視へと舵を切り、「何でも install する」 方式から、明示的に信頼したものだけを解決・実行する 方式へ移行しようとしています。(pnpm)

この記事では、防御を4つに分けて考えます。 依存解決install 実行CI 実行公開経路 min-release-age はどちらかというと依存関係に位置するかと思います。

依存解決を遅らせて固定する

まずやるべきは、「どの版を解決するか」を安定させることです。npm の min-release-age は日単位、pnpm の minimumReleaseAge は分単位で、新しく公開された版を一定時間寝かせられます。 一方でセキュリティフィックスなどの緊急で当てたいパッチやすぐにupdateをかけたい依存関係を例外にしたいケースは運用していると考えられます。

pnpm はminimumReleaseAgeExclude もあり、特定 package や version だけを例外にできます。 Dependabot には cooldownは、依存パッケージの新バージョンが公開されても、一定期間は更新 PR を作らせないための猶予設定があります。 この猶予設定は version updates だけを遅らせ、security updates には適用されません。 つまり「通常更新は寝かせるが、緊急の security fix は急ぐ」のような運用は十分に本番のプロダクトで運用可能です。(npm Docs)

ただし、更新を寝かせるだけでは十分ではありません。ある時点で解決された依存関係を、チームや CI でも同じ形で再現できなければ、環境ごとに入る版がずれてしまうからです。そこで重要になるのが lock file です。

package-lock.json は、実際に解決された依存関係とそのバージョンを固定して記録するためのファイルです。これをコミットしておくことで、開発環境や CI でも同じ依存関係を再現しやすくなります。npm ci は lock file を前提に動作し、package.json と不整合があれば失敗し、lock file も更新しません。そのため、CI では npm install より npm ci のほうが、再現性を保ちやすく、意図しない依存変更も差分として見えやすいという利点があります。(npm Docs)

さらに、lock file はセキュリティ面でも意味があります。GitHub の dependency graph では、manifest だけよりも lock file があるほうが、実際に解決された依存関係をより正確に把握しやすいためです。manifest だけから推定された indirect dependency は、vulnerability checks の対象外になる場合もあります。(GitHub Docs)

もうひとつ、別系統のリスクとして押さえておきたいのが dependency confusion です。npm は、private package と同名の public package が入り込む攻撃への対策として、scoped package の利用を強く推奨しています。内部 package を @your-org/foo のような名前空間付きで管理するのは、地味ですが効果的です。(npm Docs)

# .npmrc
min-release-age=3
ignore-scripts=true
# pnpm-workspace.yaml
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
  - '@your-org/*'

npm の min-release-age や pnpm の minimumReleaseAge を使うと、公開されたばかりの版をすぐには取り込まないようにできます。npm は日単位、pnpm は分単位で設定でき、pnpm では間接依存にも適用されます。

ただし、これはあくまで新しい版をすぐ採用しないための仕組みであって、依存関係の再現性そのものを保証するものではありません。実際に同じ依存関係を安定して再現したいなら、lockfile をコミットし、CI では npm ci や pnpm install –frozen-lockfile のような lockfile を厳密に使う運用にしておくのが基本です。(npm Docs)

install を「ダウンロード」ではなく「コード実行」とみなす

axios の件がわかりやすい例になっています。問題は Axios 本体のコードではなく、紛れ込んだ plain-crypto-jspostinstall でした。つまり npm install は単なる取得処理ではなく、依存が持つ script を通じてその場でコードを実行する行為でもあります。(Snyk)

npm には ignore-scripts があり、これを true にすると package.json に書かれた scripts を自動実行しません。npm runnpm test のように明示的に叩いた script は動くが、少なくとも install 時に dependency 側の preinstall / install / postinstall を無制限に踏む運用からは離れることができます。(npm Docs)

pnpm はここをさらに強くしています。supply chain security のガイドでは、過去の compromised package の多くが postinstall を悪用してきたとして、v10 では dependency の postinstall 自動実行を止め、allowBuilds で trusted なものだけを明示的に許可する運用を勧めている。stable docs でも allowBuilds は map で package ごとに許可・拒否を持てて、strictDepBuilds を有効にすると未レビュー build script が出た時点で install を失敗させられます。(pnpm)

さらに blockExoticSubdeps を有効にすると、間接依存が git repository や tarball URL のような exotic source を引くことを防げる。trustPolicy: no-downgrade を使えば、以前の版より trust evidence が弱い公開物を拒否することもできる。これらは全部、「仮に悪いものを引いてしまっても、勝手に広がらないようにする」ための設定です。(pnpm)

# pnpm-workspace.yaml
minimumReleaseAge: 1440
blockExoticSubdeps: true
strictDepBuilds: true
allowBuilds:
  esbuild: true
trustPolicy: no-downgrade

要するに、min-release-age は「新しい毒入り版を踏みにくくする」設定で、ignore-scriptsstrictDepBuilds は「踏んでしまっても勝手に実行させない」設定になります。(npm Docs)

GitHub Actionsを最小権限・不変参照で動かす

GitHub Actions では、まず workflow が実行するコードを不変参照で固定すること が重要です。@v1@v1.2.3 のような tag 参照は便利ですが、tag はあとから付け替えられます。GitHub も、Action を immutable に参照する方法は full-length commit SHA に pin することだけ だと明記しています。したがって、uses: owner/action@v1 ではなく、uses: owner/action@<commit SHA> のように書くのが安全側の基本です。tag のような“動く参照”に依存すると、workflow ファイル自体を変えていなくても、後から実行されるコードがすり替わる余地が残るからです。(GitHub Docs)

次に、実行時の権限も最小化します。GITHUB_TOKEN は必要最小限にとどめ、デフォルトは contents: read 程度に絞り、必要な job にだけ追加権限を与える構成が GitHub の推奨です。workflow ファイルそのものも CODEOWNERS で保護して、.github/workflows への変更にレビューを必須化できます。さらに、クラウドにアクセスする job では、長期 secret を GitHub に保存する代わりに OIDC を使えば、その場限りの短命トークンで認証できます。なお、permissions: id-token: write は外部サービス向けの OIDC トークンを取得するための設定であり、GitHub 上の操作権限を広げるものではありません。(GitHub Docs)

そのうえで、依存関係の変更自体は PR で止めるのが次の防御線です。GitHub の dependency review action は、PR に含まれる依存関係の追加・更新をチェックし、既知の脆弱性がある変更をマージ前に検出できます。レビュー画面では、追加・更新された依存関係だけでなく、release date や vulnerability data なども確認できます。たとえば次の workflow では、PR 上の依存変更に high 以上の脆弱性が含まれていれば job を失敗させられます。(GitHub Docs)

name: dependency-review

on:
  pull_request:

permissions: {}

jobs:
  review:
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<FULL_LENGTH_SHA>
      - uses: actions/dependency-review-action@<FULL_LENGTH_SHA>
        with:
          fail-on-severity: high

ただし、ここで対象は少し分けて考える必要があります。dependency review action は、主に PR で追加・更新される依存関係の安全性を確認する仕組みです。一方で GitHub は .github/workflows/ 内の uses: も dependency graph 上の依存として認識しますが、Dependabot alerts for actions が自動で出るのは semver 参照の Action に限られ、SHA pin した Action には alert が出ません。そのため実務では、外部 Action はまず SHA pinning で固定し、更新は別途、定期的な review で拾うという運用になります。つまり、普段は不変参照で安全側に倒し、上げるときだけ意識的にレビューする、という形です。(GitHub Docs)

公開経路そのものを守る

自分が npm package を公開する側なら、publish path 自体も守らないと upstream 汚染の起点になりえます。npm の trusted publishing は OIDC で publish する仕組みで、長期 npm token を CI に置かずに済みます。npm は trusted publisher を設定したあと、従来の token-based publishing を制限し、Require two-factor authentication and disallow tokens にすることを強く推奨しています。移行後は不要になった automation token を revoke する手順まで案内されています。(npm Docs)

trusted publishing を GitHub Actions や GitLab CI/CD から使うと、npm は provenance attestations も自動生成します。npm の provenance は「その package がどこで build され、誰が publish したか」を public に検証可能にする仕組みです。つまり、trusted publisher を設定した GitHub Actions から publish するなら、下記のように通常は npm publish –provenance を明示しなくても provenance が付きます(npm Docs)

name: publish

on:
  release:
    types: [published]

permissions:
  contents: read
  id-token: write

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<FULL_LENGTH_SHA>
      - uses: actions/setup-node@<FULL_LENGTH_SHA>
        with:
          node-version: "24"
          registry-url: "https://registry.npmjs.org"
      - run: npm ci
      - run: npm publish

ここで、署名と provenance は別物だと整理しておきます。npm の ECDSA registry signatures は、配布された tarball が途中で改ざんされていないかを確かめるための仕組みです。たとえば、mirror や proxy を経由する過程で package content がすり替えられていないかを検証できます。

一方 provenance は、その package がどのソースコードから、どの手順で作られ、どこから公開されたかを示す来歴情報です。つまり、署名が「届いた package が改ざんされていないか」を見るのに対して、provenance は「その package がどこで、どう作られたか」を検証できるようにする仕組みだと捉えると分かりやすいです。

なお、npm audit signatures を使うと、registry signatures と provenance attestations の両方を検証できます。ただし、これは日常的な脆弱性検知の中心というより、配布物の完全性や来歴を確かめるための補完的なチェックです。(npm Docs)

pnpm では少し踏み込み方が違います。npm のように「あとから検証する」仕組みに加えて、前述の通りblockExoticSubdepsstrictDepBuilds で信頼しにくい依存を install 時点で弾いたりできます。つまり、npm が signatures や provenance を主に検証側で扱うのに対して、pnpm は install policy で危ない取り込みをあらかじめ減らす方向にも踏み込んでいます。

横断対策: SCA で検知し、パッケージマネージャ設定で侵入を防ぐ

ここで押さえておきたいのが SCA です。SCA(Software Composition Analysis)は、プロジェクトが依存しているライブラリを可視化し、既知の脆弱性やライセンス上の問題を検知するための仕組みです。つまり、いま何を使っているのかを把握し、その中に既知のリスクがないかを継続的に確認するための土台になります。

GitHub では、この役割を dependency graph が担います。dependency graph は manifest や lock file を解析して依存関係を取り込み、graph に載った依存には Dependabot alerts や security updates を適用できます。さらに GitHub 自身も、より信頼できる graph を得るには lock file を使うべきだとしています。逆にいうと、build 時に解決される transitive dependency や、manifest だけから推定された indirect dependency には見落としが起こりえます。(GitHub Docs)

この不足を補うのが automatic dependency submission や dependency submission API です。これらを使うと、lockfile に書かれている依存だけでなく、実際の build の結果として解決された依存関係を dependency graph に送れます。GitHub には built-in workflow もあり、外部 CI/CD や custom build から snapshot を API 経由で投入することもできます。つまり、静的に見える依存だけでなく、実行時に実際に解決された依存まで graph に反映できるようになります。(GitHub Docs)

外部ツールも、この文脈で役割を分けて考えると整理しやすくなります。たとえば Snyk Open Source は、open-source dependency の脆弱性やライセンス問題を見る典型的な SCA です。OSV-Scanner は package-lock.jsonpnpm-lock.yamlyarn.lockbun.lock など、主要な JavaScript 系 lockfile を広く扱えます。Trivy は GitHub dependency snapshot を --format github で出力できるため、image や artifact から見えた依存関係を GitHub の dependency graph に寄せる導線としても使えます。(Snyk User Docs)

一方で、こうしたツールの多くは、基本的に 既知の vulnerability や advisory、ライセンス情報をどう扱うか に強いものです。これに対して Socket のようなツールは、静的解析によって install script、network request、environment variable access、telemetry、obfuscated code などを見て、まだ advisory になっていない不審な挙動を拾いにいきます。ここは役割が異なります。

大事なのは、SCA だけでは防ぎきれないという点です。既知の脆弱性は検知できても、公開されたばかりの malware や、まだ advisory が付いていない不審パッケージには時間差が生まれます。pnpm も指摘しているように、malware の公開から検知までにはどうしてもギャップがあります。だから実務では前述の通り、SCA による検知だけに頼るのではなく、minimumReleaseAgeignore-scriptsblockExoticSubdepsstrictDepBuilds のようなパッケージマネージャ側の予防設定で、そもそも危ない依存が入り込みにくく、実行されにくい状態を作っておく必要があります。(pnpm)

今日からやる最低ライン

  • .npmrcmin-release-age=3ignore-scripts=true を入れる。npm は前者を日単位の成熟期間、後者を scripts の自動実行抑止として提供している。(npm Docs)
  • lockfile を必ずコミットし、CI は npm ci を使う。npm ci は lockfile 不一致で失敗し、lockfile を書き換えない。(npm Docs)
  • private package は scope を付ける。dependency confusion の基本対策になる。(npm Docs)
  • pnpm を使うなら minimumReleaseAgeblockExoticSubdepsstrictDepBuildsallowBuilds を有効にし、必要なら trustPolicy: no-downgrade まで入れる。(pnpm)
  • GitHub Actions は full-length commit SHA pinning、GITHUB_TOKEN 最小権限、workflow 変更の CODEOWNERS レビューをセットで入れる。(GitHub Docs)
  • クラウド認証は OIDC に寄せ、id-token: write を必要な job にだけ付ける。(GitHub Docs)
  • PR には dependency review action を入れ、依存差分をマージ前に見る。依存関係の見張りは GitHub dependency graph / Dependabot を baseline にする。(GitHub Docs)
  • package公開側なら trusted publishing に移行し、従来 token を disallow して revoke する。(npm Docs)

おわりに

依存解決で待つ。install で勝手に実行させない。CI で参照先と権限を固定する。公開では長期資格情報を消し、provenance を付けて verify する。さらに SCA で差分と既知リスクを見張る。 この組み合わせになって初めて、サプライチェーン攻撃に対して防御を始めたと言えそうです。(npm Docs)


この記事は Zenn にも転載しています。