Skip to content
Published on

npmサプライチェーン攻撃の解剖 — Red Hatすら突破された時代の防御戦略

Authors

はじめに — Red Hatまでもが突破された

2026年6月、Hacker Newsの1面を飾った事件がありました。Red Hat Cloud Servicesの公式JavaScriptクライアントリポジトリであるRedHatInsights/javascript-clientsに関連する悪意あるnpmパッケージが発見されたのです。同リポジトリのイシュー492番には、疑わしいパッケージバージョンや異常なinstall scriptの挙動を報告するユーザーの書き込みが相次ぎ、GeekNewsでも翻訳要約が素早く共有され、韓国のコミュニティでも大きな話題となりました。

この事件が衝撃的だった理由は単純です。Red Hatはオープンソースエコシステムの中で最もセキュリティに敏感なベンダーの一つであり、多くのエンタープライズ顧客が信頼を前提に彼らのクライアントライブラリをインストールしているからです。「有名ベンダーの公式スコープパッケージだから安全だろう」という前提が崩れた瞬間、私たち全員のnpm installは潜在的な攻撃ベクトルになります。

実のところ、npmサプライチェーン攻撃は新しい話ではありません。event-stream(2018年)、ua-parser-js(2021年)、node-ipc(2022年)、そして2025年に連鎖的に発生した大規模トークン窃取ワーム事件まで、毎年より巧妙になっています。変わったのは攻撃対象の格です。個人メンテナーのサイドプロジェクトではなく、大手ベンダーの公式パッケージとビルドインフラが直接標的になる時代が来たのです。

本記事では、npmサプライチェーン攻撃の類型を体系的に分類し、個人開発者から組織レベルまで適用できる防御スタックを、コードと設定例を中心に整理します。

サプライチェーン攻撃の5つの類型

防御手段をマッピングするには、まず攻撃の類型を分類する必要があります。npmエコシステムで観測された攻撃は大きく5つに分かれます。

類型攻撃手法代表事例主要な防御
タイポスクワッティング類似名パッケージの登録crossenv 対 cross-env依存関係レビュー、スキャナー
アカウント乗っ取りメンテナーのnpmアカウント侵害ua-parser-js2FA強制、provenance
install scriptpostinstallで悪意あるコードを実行eslint-scopeignore-scripts
依存関係混乱社内パッケージ名を公開レジストリで先取り2021年Birsan研究スコープ固定、レジストリ設定
ビルドインフラ侵入CIやリリースパイプライン自体を侵害SolarWinds、2026年Red Hat事件隔離ビルド、署名検証

それぞれの類型をもう少し深く見ていきましょう。

1. タイポスクワッティング (Typosquatting)

最も古典的な手口です。人気パッケージと1〜2文字違う名前で悪意あるパッケージを登録し、タイプミスをした開発者がインストールするのを待ちます。変種としてコンボスクワッティングがあり、react-dom-routerのように実在するパッケージの名前を組み合わせて、もっともらしい名前を作る手法です。

2. アカウント乗っ取り (Account Takeover)

メンテナーのnpmアカウントの資格情報をフィッシングやクレデンシャルスタッフィングで入手した後、正規パッケージの新バージョンに悪意あるコードを仕込んで公開します。パッケージ名は本物なので、タイポスクワッティングよりはるかに危険です。2021年のua-parser-js事件では、週間数百万ダウンロードのパッケージに暗号通貨マイナーと情報窃取コードが混入しました。

3. install scriptの悪用

npmはパッケージのインストール時に、preinstall、install、postinstallスクリプトを自動実行します。コードを一行もimportしなくても、インストールした瞬間に任意のコードが実行されるということです。2025年の大規模ワーム事件では、悪意あるコードがまさにこの点を狙いました。開発者マシンの環境変数、npmトークン、クラウド資格情報を収集して外部に送信し、窃取したnpmトークンでさらに別のパッケージを感染させる自己複製構造でした。

4. 依存関係混乱 (Dependency Confusion)

社内専用パッケージ名が公開レジストリに登録されていない場合、攻撃者が同じ名前でより高いバージョンを公開レジストリに公開します。レジストリ優先順位の設定を誤ったビルド環境は、社内パッケージの代わりに公開された悪意あるパッケージを取得します。2021年、Alex Birsanはこの手法でAppleやMicrosoftなど35社の内部ネットワークでのコード実行を実証し、13万ドル以上のバグバウンティを獲得しました。

5. ビルドインフラ侵入

最も巧妙で防御が難しい類型です。パッケージのソースコードはクリーンなのに、ビルドパイプラインやリリース段階で悪意あるコードが注入されます。2026年のRed Hat事件が注目された理由もここにあります。単一のメンテナーではなく組織のリリース体制のどこかが侵害されると、ソースコードレビューだけでは検知が不可能です。

攻撃対象領域を一目で

  開発者マシン                レジストリ(npm)            利用者のCI/本番環境
+------------------+        +-----------------+        +--------------------+
| ソースコード       |--公開-->| パッケージtarball|--install->| node_modules     |
| npmトークン       |        | メタデータ        |        | postinstall実行    |
| .npmrc           |        | dist-tags        |        | バンドルに混入      |
+------------------+        +-----------------+        +--------------------+
        ^                          ^                          ^
        |                          |                          |
  [アカウント乗っ取り]        [タイポスクワッティング]      [install script]
  [トークン流出]             [依存関係混乱]               [lockfile迂回]
        |                          |
  [ビルドインフラ侵入] -- ソースとtarballが異なる場合、検知は非常に困難

核心的な洞察はこれです。ソースリポジトリ(GitHub)とレジストリにアップロードされたtarballは別個の成果物です。GitHubでコードをレビューしても、実際にインストールされるtarballに同じコードが入っている保証はどこにもありません。この隙間を埋めるのが、後述するprovenanceです。

第一の防衛線 — install scriptsの遮断

最も費用対効果の高い防御から始めます。プロジェクトルートまたはユーザーホームの.npmrcに以下を追加してください。

# .npmrc — install scriptの自動実行を遮断
ignore-scripts=true

# 副次効果: 依存関係混乱を防ぐためのスコープ固定
@mycompany:registry=https://npm.internal.mycompany.com/
always-auth=true

# 監査レベルの設定
audit-level=high

ignore-scriptsを有効にすると、esbuildやsharpのようにネイティブバイナリをビルドするパッケージが動作しなくなることがあります。その場合は全面遮断した上でホワイトリスト方式で許可するのが定石です。pnpmはこれを第一級機能としてサポートしています。

# pnpm v10+ — pnpm-workspace.yaml
# デフォルトで全install scriptを遮断し、明示されたパッケージのみ許可
onlyBuiltDependencies:
  - esbuild
  - sharp
  - better-sqlite3

npmを使う場合は、ignore-scriptsを有効にしたまま、必要なパッケージだけをインストール後に手動でビルドするスクリプトを用意する方式が現実的です。

{
  "scripts": {
    "postdeps": "npm rebuild esbuild sharp",
    "deps": "npm ci --ignore-scripts && npm run postdeps"
  }
}

2025年のワーム事件以降、pnpmがinstall scriptをデフォルト遮断に変更したことは、エコシステム全体の方向性を示しています。実行されるコードの総量を減らすことが、すべての防御の出発点です。

第二の防衛線 — lockfileの完全性

lockfileは単なるバージョン固定装置ではなく、完全性検証装置です。package-lock.jsonの各エントリにはintegrityフィールドがあります。

{
  "node_modules/lodash": {
    "version": "4.17.21",
    "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
    "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
  }
}

integrityはtarballのSHA-512ハッシュです。npm ciはこのハッシュが一致しない場合、インストールを中断します。したがってCIでは必ずnpm installではなくnpm ciを使うべきです。npm installはlockfileを静かに更新してしまう可能性がありますが、npm ciはlockfileとpackage.jsonが食い違うと失敗するからです。

これに2つの対策を加えると、lockfile防御が完成します。

第一に、lockfile自体の改ざんを検証します。PRでlockfileのresolved URLが公式レジストリ以外を指すように書き換えられる攻撃が実際に報告されています。lockfile-lintで遮断します。

npx lockfile-lint \
  --path package-lock.json \
  --allowed-hosts npm \
  --validate-https \
  --validate-integrity

第二に、PRでの依存関係の変更を人間が確認できるようにします。GitHubのdependency-review-actionは、PRに追加された依存関係の既知の脆弱性とライセンスを検査します。

# .github/workflows/dependency-review.yml
name: Dependency Review
on: [pull_request]

permissions:
  contents: read

jobs:
  dependency-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high
          deny-licenses: AGPL-1.0-only, AGPL-3.0-only

第三の防衛線 — provenanceとSigstore署名

先述の「ソースとtarballの隙間」問題を解決するのがnpm provenanceです。2023年から、npmはSigstoreベースのprovenance証明をサポートしています。パッケージがどのリポジトリのどのコミットから、どのCIワークフローでビルドされたかを暗号学的に証明するメタデータです。

公開側(メンテナー)は、GitHub Actionsで次のように設定します。

# .github/workflows/publish.yml
name: Publish Package
on:
  release:
    types: [published]

permissions:
  contents: read
  id-token: write   # Sigstore OIDC署名に必須

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci --ignore-scripts
      - run: npm run build
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

肝心なのはid-token権限です。このワークフローはGitHub OIDCトークンでSigstoreのFulcio証明書の発行を受けて署名し、署名記録は公開透明性ログ(Rekor)に残ります。長期保管の署名鍵がそもそも存在しないため、鍵流出という攻撃ベクトル自体が消滅します。

利用側では、インストールされたパッケージの署名と証明を検証できます。

# レジストリ署名とprovenance証明を一括検証
npm audit signatures

provenanceの限界も知っておくべきです。provenanceは「このtarballはこのリポジトリのこのコミットからビルドされた」ことを証明するだけで、そのコミットのコードが安全であることを証明するわけではありません。ビルドインフラ侵入型の攻撃を困難にしますが、悪意あるコミット自体を防ぐことはできません。また2026年現在でも、provenance付きで公開されているパッケージは全体のごく一部にすぎず、「provenanceのないパッケージを遮断」というポリシーを一括適用するのはまだ困難です。

依存関係の最小化とベンダリング論争

技術的な防御の前に、構造的な問いがあります。そもそも依存関係はこんなに多くなければならないのでしょうか?

平均的なNode.jsプロジェクトは、直接依存関係数十個、推移的依存関係千個以上を引き込みます。left-pad騒動が示したように、一行の関数のためにパッケージをインストールする文化が、攻撃対象領域を指数関数的に拡大させました。

実務で使える判断基準を整理するとこうなります。

状況推奨
20行以内で自前実装可能自分で書く (依存関係の追加禁止)
標準ライブラリで代替可能Node内蔵のfetch、structuredCloneなどを使用
小さいが検証が重要なロジックコードをコピーしてベンダリング + 出典コメント
大きく複雑なライブラリ依存関係として追加 + Scorecardのスコア確認

ベンダリング(依存関係のソースをリポジトリに直接コピー)をめぐる論争は、HNで周期的に繰り返されます。賛成派はサプライチェーン攻撃の遮断とビルドの再現性を、反対派はセキュリティパッチの取りこぼしと保守負担を指摘します。バランスポイントは明確です。クリティカルパス上の小さなユーティリティはベンダリングし、活発にパッチされる大型ライブラリは自動化された更新パイプラインとともに依存関係として維持することです。

新しい依存関係を評価する際は、OpenSSF Scorecardが客観的な指標を与えてくれます。

# 依存関係候補のセキュリティ衛生チェック
npx @ossf/scorecard-cli --repo=github.com/sindresorhus/got

# 主なチェック項目:
# - Maintained: 直近90日以内のコミット/イシュー活動
# - Code-Review: PRレビューの強制有無
# - Signed-Releases: リリース署名の有無
# - Dangerous-Workflow: CI設定の危険パターン
# - Token-Permissions: GitHubトークンの最小権限

組織レベルの防御スタック

個人の注意力に依存するセキュリティは必ず失敗します。組織であれば、次の4層スタックを推奨します。

+--------------------------------------------------------------+
| 第4層: ポリシー/監査   OpenSSF Scorecard、SBOM、定期audit      |
+--------------------------------------------------------------+
| 第3層: スキャナー      Socket / Snyk — 振る舞いベース検知       |
+--------------------------------------------------------------+
| 第2層: 更新制御        Renovate遅延ポリシー (熟成期間)          |
+--------------------------------------------------------------+
| 第1層: レジストリ      社内プロキシレジストリ (検疫所の役割)      |
+--------------------------------------------------------------+

第1層 — 社内プロキシレジストリ

Verdaccio、JFrog Artifactory、Sonatype Nexusのようなプロキシレジストリを置くと、組織のすべてのnpmトラフィックが単一の関門を通過します。悪意あるパッケージが発見されたら関門で一括遮断でき、アップストリームが障害でもキャッシュでビルドが維持されます。

# verdaccio config.yaml (抜粋)
uplinks:
  npmjs:
    url: https://registry.npmjs.org/
    cache: true

packages:
  '@mycompany/*':
    access: $authenticated
    publish: $authenticated
    # 社内スコープはアップストリーム照会禁止 — 依存関係混乱を根本から遮断
  '**':
    access: $all
    proxy: npmjs

社内スコープパッケージにproxy設定を入れないことが、依存関係混乱防御の核心です。社内の名前が公開レジストリに流出して解決される経路を断つのです。

第2層 — Renovate遅延ポリシー

サプライチェーン攻撃の時間パターンを見ると、悪意あるバージョンが公開されてから発見されて削除されるまで、通常数時間から数日かかります。逆に言えば、新バージョンを即座に取得せず一定期間熟成させるだけで、ほとんどの攻撃を回避できます。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "minimumReleaseAge": "7 days",
  "internalChecksFilter": "strict",
  "packageRules": [
    {
      "matchUpdateTypes": ["patch", "minor"],
      "minimumReleaseAge": "7 days"
    },
    {
      "matchUpdateTypes": ["major"],
      "minimumReleaseAge": "14 days"
    },
    {
      "matchDepTypes": ["devDependencies"],
      "minimumReleaseAge": "3 days"
    }
  ],
  "vulnerabilityAlerts": {
    "minimumReleaseAge": "0 days"
  }
}

最後のブロックが重要です。セキュリティパッチは遅延なしで即座に受け取るよう例外を設けないと、遅延ポリシーが逆に脆弱性の露出期間を延ばす副作用を招きます。

第3層 — 振る舞いベースのスキャナー

従来の脆弱性スキャナー(CVEデータベース照合)は既知の脆弱性しか捕捉できません。サプライチェーン攻撃は既知になる前に被害をもたらすため、Socketのようにパッケージの振る舞いを分析するツールが補完として必要です。新バージョンに突然ネットワーク呼び出し、環境変数アクセス、難読化コード、install scriptが追加されると、PR段階で警告します。

# socket.yml — プロジェクトルート
version: 2
issueRules:
  installScripts: error
  obfuscatedFile: error
  envVars: warn
  networkAccess: warn
  shellAccess: error

第4層 — SBOMと定期監査

侵害事故が起きたとき、「我々はそのパッケージを使っているか、どのサービスで?」に5分以内に答えられなければなりません。CycloneDXまたはSPDX形式のSBOMをビルドごとに生成して保管してください。

# CycloneDX SBOMの生成
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

# 特定パッケージの使用有無を即座に照会
jq '.components[] | select(.name == "compromised-pkg") | .version' sbom.json

CIでの隔離 — ネットワーク遮断ビルド

install scriptを許可せざるを得ない環境であれば、実行されても被害がないように隔離すべきです。鍵となるのはビルド段階のネットワークエグレス制御です。

GitHub Actionsではharden-runnerが標準的な選択肢です。

# .github/workflows/build.yml
name: Hardened Build
on: [push, pull_request]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: step-security/harden-runner@v2
        with:
          egress-policy: block
          allowed-endpoints: >
            registry.npmjs.org:443
            npm.internal.mycompany.com:443
            github.com:443
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci --ignore-scripts
      - run: npm run build
      - run: npm test

egress-policyをblockにすると、ホワイトリスト外のすべてのアウトバウンド接続が遮断されログに残ります。2025年のワーム事件の際、窃取コードが資格情報を外部サーバーに送信する段階でまさにこの防御に引っかかったという報告が多数ありました。

セルフホスト環境であれば、コンテナレベルで同じ制御を実装します。

# ネットワークなしビルドコンテナ — 依存関係は事前キャッシュボリュームから
docker run --rm \
  --network=none \
  -v "$PWD:/app" \
  -v npm-cache:/root/.npm \
  -w /app \
  node:22-slim \
  sh -c "npm ci --offline --ignore-scripts && npm run build"

npm ci --offlineはキャッシュにないパッケージに遭遇すると失敗するため、キャッシュ充填(ネットワーク許可、スクリプト遮断)とビルド(ネットワーク遮断)を2段階に分離するパターンになります。

さらに、CIで使うnpmトークン自体をなくすことも重要です。読み取り専用ミラーを使うか、公開段階でのみOIDCベースの短期トークンを発行する構造にすれば、CIが侵害されても窃取される長期トークンが存在しません。

インシデント対応手順

防御が突破されたときの手順を事前に文書化しておかないと、事故当日に即興で動くことになります。最低限のrunbookは次のとおりです。

[T+0分] 検知
  - スキャナー警告、HN/セキュリティ公告、または内部の異常兆候で認知
  - 影響パッケージ名と悪意あるバージョン範囲を確定

[T+15分] 露出評価
  - SBOM照会: 該当パッケージを使うサービス/リポジトリの一覧を抽出
  - lockfile履歴照会: 悪意あるバージョンが実際にインストールされた時点を確認
  - 悪意あるバージョンの挙動把握: トークン窃取型か、バックドア型か

[T+30分] 隔離
  - 社内レジストリで悪意あるバージョンを遮断
  - 影響リポジトリのCIパイプラインを一時停止
  - デプロイ済み成果物のうち悪意あるバージョンを含むものを特定

[T+1時間] 資格情報のローテーション
  - 悪意あるバージョンが実行された環境のすべてのシークレットを無効化
  - npmトークン、クラウドキー、CIシークレット、SSHキーの順にローテーション
  - トークン窃取型ならgit push履歴とnpm publish履歴を監査

[T+1日] 復旧と事後分析
  - 安全なバージョンに固定後、lockfileを再生成
  - タイムラインを文書化し、検知の空白を特定
  - 再発防止項目を防御スタックに反映

資格情報ローテーション段階でよくある間違いは、「うちのコードはそのパッケージの関数を使っていないから大丈夫」という判断です。install script型の攻撃は、コードの使用有無とは無関係にインストール時点ですでに実行されています。インストール履歴があれば、露出したものとみなすべきです。

実践チェックリスト

個人開発者向け:

[ ] .npmrcにignore-scripts=trueを設定
[ ] npm installの代わりにnpm ciを習慣化 (CIでは強制)
[ ] npmアカウントの2FAを有効化 (可能ならハードウェアキー)
[ ] ローカルに永続npmトークンを保管しない (granular token + 有効期限設定)
[ ] 新しい依存関係の追加前にダウンロード推移、メンテナー、Scorecardを確認
[ ] npm audit signaturesを定期的に実行

組織向け:

[ ] 社内プロキシレジストリを導入し、社内スコープのアップストリーム照会を遮断
[ ] Renovate minimumReleaseAgeを7日以上 + セキュリティパッチの例外
[ ] 振る舞いベースのスキャナーをPRゲートとして設置
[ ] CIエグレスポリシーをblock + ホワイトリスト
[ ] ビルドごとにSBOMを生成・保管
[ ] 公開パッケージにprovenance署名を適用
[ ] インシデント対応runbookの作成と四半期ごとの模擬訓練
[ ] lockfile-lint + dependency-reviewを必須チェックとして登録

落とし穴と反論 — 批判的に見る

本記事の防御スタックにも限界と反論があります。正直に押さえておきましょう。

第一に、ignore-scriptsは万能ではありません。install scriptを防いでも、悪意あるコードがパッケージ本体に入っていればrequireした瞬間に実行されます。install scriptの遮断は攻撃の自動実行段階を防ぐだけで、コード自体の信頼問題を解決するわけではありません。

第二に、遅延ポリシーは確率のゲームです。7日間の熟成は早く発見される攻撃にのみ有効です。数ヶ月潜伏する巧妙なバックドア(xz-utils事件を思い出してください)には無力です。それでも統計的にnpmの悪意あるパッケージのほとんどが数日以内に発見されるという点で、費用対効果は依然として大きいです。

第三に、ツールスタック自体が新たな依存関係です。スキャナーベンダー、レジストリプロキシ、CIセキュリティアクションも結局は第三者のコードであり、侵害される可能性があります。harden-runnerのようなセキュリティツールを検証なしに追加するのは皮肉な話なので、セキュリティツールこそバージョン固定(SHAピン)と出所検証を厳格に行うべきです。

# アクションはタグではなくコミットSHAで固定
- uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0

第四に、開発者体験のコストは実在します。熟成期間のせいで新機能バージョンをすぐに使えず、ネットワーク遮断ビルドはデバッグを難しくします。セキュリティポリシーが迂回文化を生むと(個人のノートPCでビルドしてアップロードするなど)、かえってリスクが増大します。ポリシー導入時に例外手続きを公式化することが重要です。

第五に、根本的にこれはエコシステム構造の問題です。個別組織の防御には限界があり、npmレジストリレベルでの2FA強制の拡大、provenanceのデフォルト化、install scriptのデフォルト遮断といった構造的変化が並行して進む必要があります。OpenSSFとnpmはその方向に動いていますが、速度は緩慢です。

おわりに

2026年のRed Hat事件は、「信頼できるベンダー」という概念そのものを再定義させました。信頼は名前から来るのではなく、検証可能な証拠(署名、provenance、振る舞い分析)から来るべきです。

要約するとこうなります。

  1. 今日すぐに: ignore-scriptsとnpm ci、2FA。ほぼコストゼロで最大の攻撃ベクトルを減らせます。
  2. 今四半期中に: Renovate遅延ポリシー、lockfile-lint、dependency-review、スキャナーゲート。
  3. 年内に: 社内レジストリ、CIエグレス制御、SBOM、provenance署名、インシデント対応訓練。

サプライチェーンセキュリティは一度きりの導入ではなく運用です。完璧な防御はありませんが、攻撃者のコストを十分に高めることは可能です。そしてそのコストを高める作業のほとんどは、上で見たように設定ファイルの数行から始まります。

参考資料