LabHub

ブログ

Python PyPI サプライチェーン攻撃防御戦略:タイポスクワッティングから多層セキュリティまで

한국어English日本語

はじめに: なぜ今PyPIサプライチェーンセキュリティなのか

2025年下半期から2026年初頭にかけて、Python PyPIを対象としたサプライチェーン攻撃が前例のない水準で急増しました。2025年7月から2026年1月の間だけで128個のファントムパッケージが合計121,539回ダウンロードされ、週平均3,903回の悪意あるインストールが発生しました。The Hacker Newsによれば、2026年2月のdYdXサプライチェーン攻撃のように、暗号資産ウォレットの窃取とRAT(リモートアクセス型トロイの木馬)の配布を組み合わせた高度化した攻撃が登場しています。

本記事では実際に発生した攻撃事例を分析し、開発チームがすぐに適用できる多層防御戦略を実践的なコードとともに提示します。

PyPIサプライチェーン攻撃の類型分析

攻撃類型の比較表

攻撃類型説明代表事例危険度
タイポスクワッティング(Typosquatting)有名パッケージ名のタイポ変種を登録termncolor(termcolor偽装)、sisaws(sisa偽装)
依存性混乱(Dependency Confusion)内部パッケージと同名の公開パッケージを登録企業の内部パッケージ名の乗っ取り非常に高い
悪意あるビルドスクリプトsetup.py/pyproject.tomlのビルドフックに悪意あるコードを挿入インストール時に自動実行されるバックドア
アカウント乗っ取り(Account Hijacking)パッケージ管理者アカウントを奪って悪意あるバージョンを配布dYdX(2026.02)、Ultralytics(2024.12)非常に高い
ファントムパッケージ(Phantom Package)有用に見える偽パッケージを大量に登録AI/MLツールを偽装したパッケージ
StarJacking人気GitHubリポジトリのURLを盗用して信頼性を偽装PyPIメタデータの改ざん

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

最も頻度の高い攻撃類型です。攻撃者はrequestsの代わりにreqeustscoloramaの代わりにcolorizrのように、有名パッケージと似た名前で悪意あるパッケージを登録します。2025年7月に発見されたtermncolorは正規のtermcolorパッケージを偽装しており、sisawssecmeasureパッケージはSilentSync RATを配布することが確認されました。

PyPIは現在、プロジェクト作成時にタイポスクワッティングの試みを自動検出してフラグを立てる機能を導入していますが、すべての変種を遮断できるわけではありません。

2. 依存性混乱(Dependency Confusion)

2021年にAlex Birsanが最初に公開したこの攻撃は、企業が内部的に使用する非公開パッケージ名と同じ名前のパッケージを公開PyPIに登録する手口です。pipは既定で公開インデックスの高いバージョン番号を優先してインストールするため、攻撃者が9999.0.0のような極端に高いバージョンを登録すると、内部パッケージの代わりに悪意あるパッケージがインストールされます。

3. アカウント乗っ取りによる正規パッケージの改ざん

最も破壊力の大きい攻撃類型です。正規のパッケージの管理者認証情報を奪い、悪意あるバージョンを配布します。この場合はパッケージ名そのものが正規のものなので、検出が非常に困難です。

失敗事例の分析

事例1: dYdXサプライチェーン攻撃(2026年2月)

2026年1月28日に公開されたこの事件では、攻撃者が暗号資産の分散型取引所dYdXの開発者認証情報を奪い、npmパッケージ(@dydxprotocol/v4-client-js)とPyPIパッケージ(dydx-v4-client)に悪意あるバージョンを配布しました。

攻撃の特徴:

教訓: dYdXは利用者に対し、感染したマシンを隔離し、クリーンなシステムで新しいウォレットへ資金を移し、すべてのAPIキーと認証情報を交換するよう勧告しました。この事例は2FA(二要素認証)とTrusted Publisher設定の重要性を示しています。

事例2: Ultralyticsサプライチェーン攻撃(2024年12月)

世界最高のコンピュータビジョンAIライブラリであるUltralytics(YOLO)が、GitHub Actionsワークフローの侵害を通じて攻撃されました。

攻撃のタイムライン:

攻撃のメカニズム:

教訓: PyPI APIトークンの範囲を特定のプロジェクトとバージョンに限定し、GitHub Actionsワークフローで外部入力(ブランチ名、PRタイトルなど)を検証する必要があります。また、Trusted Publisherを使えばトークンの窃取そのものを防げます。

多層防御戦略

┌──────────────────────────────────────────────────────────────┐
PyPI サプライチェーン多層防御                   │
├──────────┬──────────────┬──────────────┬─────────────────────┤
Layer 1Layer 2Layer 3Layer 4│          │              │              │                     │
│ 依存関係 │ 脆弱性       │ ビルド環境   │ ランタイム          │
│ 管理     │ スキャン     │ セキュリティ │ 監視                │
│          │              │              │                     │
Lockfile │ pip-audit    │ TrustedSBOMPinning  │ safety       │ Publisher    │ 追跡                │
│          │              │              │                     │
HashGitHubPEP 740      │ 依存関係            │
│ 検証     │ DependabotAttestation  │ 監査                │
│          │              │              │                     │
PrivateSnyk /       │ 2FA /        │ 異常検知            │
IndexSocket.devOIDC         │                     │
└──────────┴──────────────┴──────────────┴─────────────────────┘

Layer 1: 依存関係管理の強化

Lockfile Pinningとハッシュ検証

依存関係を正確なバージョンとハッシュで固定すれば、パッケージが改ざんされたときにインストールを遮断できます。

# pyproject.toml - uv/pip互換の依存関係管理
[project]
name = "my-secure-app"
requires-python = ">=3.11"
dependencies = [
    "requests==2.31.0",
    "cryptography==42.0.5",
    "pydantic==2.6.1",
]

[tool.uv]
# プライベートインデックスを優先する設定(依存性混乱への防御)
index-url = "https://my-company.jfrog.io/pypi/simple/"
extra-index-url = "https://pypi.org/simple/"

[tool.uv.pip]
# ハッシュ検証を必須化
require-hashes = true
# requirements.txt - ハッシュ固定の例
requests==2.31.0 \
    --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7f0edf3fcb0fce8aea3fbd5951d bdf0f4
cryptography==42.0.5 \
    --hash=sha256:6e2b11c55d260d03a8cf29ac9b5e0608c3cb2b6f56af2f20f2132764710 68e5c
pydantic==2.6.1 \
    --hash=sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5 d1fa9

プライベートインデックス優先の設定(依存性混乱への防御)

# pip.conf - プライベートインデックス優先の設定
[global]
index-url = https://my-company.jfrog.io/pypi/simple/
extra-index-url = https://pypi.org/simple/

[install]
# ハッシュ検証を既定で有効化
require-hashes = true

uvを使う場合は、より強力な依存性混乱への防御が可能です。

# pyproject.toml - uvのインデックス戦略設定
[tool.uv]
# "first-match"戦略: 最初のインデックスでパッケージが見つかれば他のインデックスは検索しない
index-strategy = "first-match"

[[tool.uv.index]]
name = "internal"
url = "https://my-company.jfrog.io/pypi/simple/"
default = true

[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple/"

Layer 2: 脆弱性スキャン

pip-auditで既知の脆弱性を検査

pip-auditはGoogleが支援しTrail of Bitsが開発したオープンソースツールで、PyPI JSON APIを通じてPython Packaging Advisory Databaseの脆弱性情報を照会します。

# pip-auditのインストールと実行
pip install pip-audit

# 現在の環境をスキャン
pip-audit

# requirements.txtに基づくスキャン
pip-audit -r requirements.txt

# 脆弱性の自動修正(安全な最新バージョンへアップグレード)
pip-audit --fix

# JSON形式で出力(CI/CDパイプライン連携用)
pip-audit -f json -o audit-report.json

# 特定の脆弱性を無視(誤検知または非該当のケース)
pip-audit --ignore-vuln PYSEC-2024-XXXX

Safety CLIで悪意あるパッケージを検出

Safetyは脆弱性検査のほかに、悪意あるパッケージの検出機能も提供します。

# Safetyのインストールと実行
pip install safety

# 現在の環境をスキャン
safety check

# requirements.txtに基づくスキャン
safety check -r requirements.txt

# JSONの出力形式
safety check --output json

# プロジェクトディレクトリ全体をスキャン(悪意あるパッケージの検出を含む)
safety scan --target ./my-project/

脆弱性スキャンツールの比較

機能pip-auditSafety CLISnyk
脆弱性DBPyPI Advisory DB (OSV)SafetyDB (PyUp)Snyk Vulnerability DB
悪意あるパッケージの検出非対応対応対応
自動修正対応(--fix非対応対応
ライセンス検査非対応有料版で対応対応
CVSSスコア非対応有料版で対応対応
CI/CD統合GitHub Actionsを提供GitHub Actionsを提供ネイティブ統合
費用無料(Apache 2.0)無料/有料無料/有料
推奨用途CI/CDの自動検査開発環境のセキュリティエンタープライズ

Layer 3: ビルド環境のセキュリティ - Trusted PublisherとPEP 740

Trusted Publisherの設定

PyPI Trusted PublisherはOpenID Connect(OIDC)を使い、GitHub ActionsなどのCI/CDプラットフォームからトークンなしで安全にパッケージを配布できるようにします。APIトークンが存在しないため、窃取そのものが不可能です。

# .github/workflows/publish.yml - Trusted Publisherベースの配布
name: Publish to PyPI

on:
  release:
    types: [published]

permissions:
  id-token: write # OIDCトークンの発行に必要
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install build dependencies
        run: pip install build

      - name: Build package
        run: python -m build

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        # Trusted Publisher使用時はpassword/tokenが不要
        # PyPIでGitHubリポジトリをTrusted Publisherとして登録する必要がある
        with:
          attestations: true # PEP 740デジタルアテステーションを自動生成

  verify:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Verify attestation
        run: |
          pip install pypi-attestations
          python -m pypi_attestations verify my-package

PEP 740デジタルアテステーション

PEP 740はPyPIパッケージに対する暗号学的に検証可能な証明(Attestation)を定義します。Sigstoreベースのキーレス(keyless)署名を使用し、パッケージがどのソースリポジトリでビルドされたかを検証できます。

# アテステーションの検証(利用者側)
pip install pypi-attestations

# 特定パッケージのアテステーションを確認
python -c "
import requests
resp = requests.get(
    'https://pypi.org/integrity/requests/2.31.0/'
)
attestations = resp.json()
print(f'Attestation count: {len(attestations)}')
for att in attestations:
    print(f'  Publisher: {att.get(\"publisher\", \"unknown\")}')
"

Layer 4: CI/CDセキュリティパイプラインの構築

GitHub Actions総合セキュリティパイプライン

# .github/workflows/security.yml
name: Python Supply Chain Security

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    # 毎日午前9時(KST)に定期スキャン
    - cron: '0 0 * * *'

permissions:
  contents: read
  security-events: write

jobs:
  dependency-audit:
    name: Dependency Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          pip install pip-audit safety cyclonedx-bom

      - name: Run pip-audit
        run: |
          pip-audit -r requirements.txt \
            -f json \
            -o pip-audit-report.json \
            --desc on
        continue-on-error: false

      - name: Run Safety check
        run: |
          safety check -r requirements.txt \
            --output json \
            > safety-report.json
        continue-on-error: true

      - name: Check for critical vulnerabilities
        run: |
          python3 -c "
          import json, sys
          with open('pip-audit-report.json') as f:
              report = json.load(f)
          vulns = report.get('dependencies', [])
          critical = [v for v in vulns if v.get('vulns')]
          if critical:
              print(f'CRITICAL: {len(critical)} vulnerable packages found')
              for pkg in critical:
                  name = pkg['name']
                  version = pkg['version']
                  for vuln in pkg['vulns']:
                      vid = vuln['id']
                      fix = vuln.get('fix_versions', ['N/A'])
                      print(f'  - {name}=={version}: {vid} (fix: {fix})')
              sys.exit(1)
          print('No vulnerabilities found')
          "

      - name: Upload audit reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: security-reports
          path: |
            pip-audit-report.json
            safety-report.json

  sbom-generation:
    name: Generate SBOM
    runs-on: ubuntu-latest
    needs: dependency-audit
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install cyclonedx-bom

      - name: Generate CycloneDX SBOM
        run: |
          cyclonedx-py environment \
            --output sbom.json \
            --output-format json \
            --schema-version 1.5

      - name: Validate SBOM
        run: |
          python3 -c "
          import json
          with open('sbom.json') as f:
              sbom = json.load(f)
          components = sbom.get('components', [])
          print(f'SBOM generated: {len(components)} components')
          print(f'Format: CycloneDX {sbom.get(\"specVersion\", \"unknown\")}')
          for comp in components[:5]:
              name = comp.get('name', 'unknown')
              version = comp.get('version', 'unknown')
              print(f'  - {name}@{version}')
          if len(components) > 5:
              print(f'  ... and {len(components) - 5} more')
          "

      - name: Upload SBOM
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.json

  lockfile-integrity:
    name: Lockfile Integrity Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Verify hash-pinned dependencies
        run: |
          pip install --require-hashes \
            -r requirements.txt \
            --dry-run \
            --no-deps
        continue-on-error: false

      - name: Check for unpinned dependencies
        run: |
          python3 -c "
          import re, sys
          unpinned = []
          with open('requirements.txt') as f:
              for line in f:
                  line = line.strip()
                  if line and not line.startswith('#'):
                      if '==' not in line and '--hash' not in line:
                          unpinned.append(line)
          if unpinned:
              print('WARNING: Unpinned dependencies found:')
              for dep in unpinned:
                  print(f'  - {dep}')
              sys.exit(1)
          print('All dependencies are version-pinned')
          "

SBOMの生成と管理

SBOM(Software Bill of Materials)はソフトウェアに含まれるすべての構成要素を文書化した一覧です。米国大統領令14028号以降、サプライチェーンの透明性の中核要素となりました。

# CycloneDXでPythonプロジェクトのSBOMを生成
pip install cyclonedx-bom

# 現在の仮想環境に基づくSBOM生成
cyclonedx-py environment \
  --output sbom.json \
  --output-format json \
  --schema-version 1.5

# requirements.txtに基づくSBOM生成
cyclonedx-py requirements \
  --input-file requirements.txt \
  --output sbom-requirements.json \
  --output-format json

# SPDX形式でも生成可能
pip install spdx-tools
# sbom_validator.py - SBOMの検証および分析スクリプト
import json
import sys
from datetime import datetime


def validate_sbom(sbom_path: str) -> dict:
    """SBOMファイルを検証し、要約レポートを生成します。"""
    with open(sbom_path) as f:
        sbom = json.load(f)

    components = sbom.get("components", [])
    metadata = sbom.get("metadata", {})

    report = {
        "timestamp": datetime.now().isoformat(),
        "spec_version": sbom.get("specVersion", "unknown"),
        "total_components": len(components),
        "components_without_version": [],
        "components_without_license": [],
        "components_without_purl": [],
    }

    for comp in components:
        name = comp.get("name", "unknown")
        if not comp.get("version"):
            report["components_without_version"].append(name)
        if not comp.get("licenses"):
            report["components_without_license"].append(name)
        if not comp.get("purl"):
            report["components_without_purl"].append(name)

    # 検証結果の出力
    print(f"SBOM Validation Report")
    print(f"=" * 50)
    print(f"Spec Version: {report['spec_version']}")
    print(f"Total Components: {report['total_components']}")
    print(f"Missing Versions: {len(report['components_without_version'])}")
    print(f"Missing Licenses: {len(report['components_without_license'])}")
    print(f"Missing PURLs: {len(report['components_without_purl'])}")

    # 品質スコアの計算
    total = report["total_components"]
    if total > 0:
        quality_score = (
            1
            - (
                len(report["components_without_version"])
                + len(report["components_without_license"])
                + len(report["components_without_purl"])
            )
            / (total * 3)
        ) * 100
        print(f"Quality Score: {quality_score:.1f}%")
        report["quality_score"] = quality_score

    return report


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python sbom_validator.py sbom.json")
        sys.exit(1)
    validate_sbom(sys.argv[1])

pyproject.tomlセキュリティ設定のベストプラクティス

[project]
name = "my-secure-app"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
    "requests>=2.31.0,<3.0",
    "cryptography>=42.0.0,<43.0",
    "pydantic>=2.6.0,<3.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

# セキュリティ関連ツールの設定
[tool.pip-audit]
# pip-auditの設定
desc = "on"
progress-spinner = "on"
output = "json"

[tool.safety]
# Safety CLIの設定
output = "json"
continue-on-error = false

[tool.ruff]
# セキュリティ関連のリントルールを有効化
select = [
    "S",     # flake8-bandit(セキュリティ脆弱性の検出)
    "B",     # flake8-bugbear
]

[tool.bandit]
# Bandit静的セキュリティ分析の設定
exclude_dirs = ["tests", "venv"]
skips = []

追加の防御手法

setup.pyビルドスクリプトの検査

悪意あるパッケージの相当数がsetup.pyinstallフックに悪意あるコードを挿入します。パッケージのインストール前にsetup.pyの内容を検査する習慣が必要です。

# パッケージのインストール前にソースコードを検査
pip download --no-binary :all: --no-deps suspect-package
# ダウンロードしたソースを展開してからsetup.pyを検査

# あるいはpipの--no-build-isolationオプションでビルドスクリプトの実行を制限
pip install --no-build-isolation --only-binary :all: package-name

GitHub Dependabotの設定

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: 'pip'
    directory: '/'
    schedule:
      interval: 'daily'
    reviewers:
      - 'security-team'
    labels:
      - 'dependencies'
      - 'security'
    open-pull-requests-limit: 10
    # セキュリティ更新のみ自動でPRを作成
    allow:
      - dependency-type: 'direct'
    # メジャーバージョンの更新は手動でレビュー
    ignore:
      - dependency-name: '*'
        update-types: ['version-update:semver-major']

pre-commitフックによるローカル検査

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pypa/pip-audit
    rev: v2.7.3
    hooks:
      - id: pip-audit
        args: ['-r', 'requirements.txt']

  - repo: https://github.com/PyCQA/bandit
    rev: 1.7.8
    hooks:
      - id: bandit
        args: ['-r', 'src/', '-ll']

  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

セキュリティチェックリスト

プロジェクトにすぐ適用できるサプライチェーンセキュリティのチェックリストです。

依存関係の管理

CI/CDのセキュリティ

アカウントのセキュリティ

モニタリング

結論

PyPIサプライチェーン攻撃は、単一のツールや単一のポリシーでは防げません。依存関係のロック(Lockfile Pinning + Hash Verification)、脆弱性スキャン(pip-audit + Safety)、ビルド環境のセキュリティ(Trusted Publisher + PEP 740 Attestation)、ランタイムのモニタリング(SBOM追跡)を組み合わせた多層防御戦略が必要です。

特に2026年現在で注目すべき3つの中核的な施策は次のとおりです。

  1. Trusted Publisherへの移行: PyPI APIトークンの代わりにOIDCベースのTrusted Publisherを使い、認証情報が奪われるリスクを根本から取り除く
  2. PEP 740アテステーションの活用: パッケージの出所を暗号学的に検証し、改ざんの有無を確認する
  3. SBOMの自動化: ビルドごとにSBOMを生成し、継続的に脆弱性モニタリングを実施する

サプライチェーンセキュリティは一度設定して終わりではなく、継続的に更新し監視しなければならない運用プロセスです。今日さっそく上のチェックリストをもとに、チームのセキュリティ状態を点検してみてください。

参考資料

コメント

まだコメントはありません。

ログインするとコメントできます