- はじめに: なぜ今PyPIサプライチェーンセキュリティなのか
- PyPIサプライチェーン攻撃の類型分析
- 失敗事例の分析
- 多層防御戦略
- pyproject.tomlセキュリティ設定のベストプラクティス
- 追加の防御手法
- セキュリティチェックリスト
- 結論
- 参考資料
はじめに: なぜ今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の代わりにreqeusts、coloramaの代わりにcolorizrのように、有名パッケージと似た名前で悪意あるパッケージを登録します。2025年7月に発見されたtermncolorは正規のtermcolorパッケージを偽装しており、sisawsとsecmeasureパッケージは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)に悪意あるバージョンを配布しました。
攻撃の特徴:
- PyPIパッケージには100回繰り返し難読化された悪意あるコードが挿入された
- npmとPyPIの両方に同時配布するクロスエコシステム攻撃
- 暗号資産ウォレットの認証情報の窃取 + RAT(リモートアクセス型トロイの木馬)の同時配布
- パッケージ管理インフラに直接アクセスし、正規のビルドプロセスを迂回
教訓: dYdXは利用者に対し、感染したマシンを隔離し、クリーンなシステムで新しいウォレットへ資金を移し、すべてのAPIキーと認証情報を交換するよう勧告しました。この事例は2FA(二要素認証)とTrusted Publisher設定の重要性を示しています。
事例2: Ultralyticsサプライチェーン攻撃(2024年12月)
世界最高のコンピュータビジョンAIライブラリであるUltralytics(YOLO)が、GitHub Actionsワークフローの侵害を通じて攻撃されました。
攻撃のタイムライン:
- 1次攻撃(12月4-5日): 悪意あるバージョン8.3.41、8.3.42を配布(約12時間の露出)
- 2次攻撃(12月7日): GitHub Actionsを迂回して直接PyPIにバージョン8.3.45、8.3.46を配布
攻撃のメカニズム:
- 攻撃者がgitブランチ名を悪用してGitHub Actions CI/CDパイプラインの認証情報を窃取
- 奪ったPyPI APIトークンでXMRig(Monero暗号資産マイナー)を含む悪意あるバージョンを配布
- この攻撃手法(Pwn Request)は2021年から知られていた方式だったが防御されていなかった
教訓: PyPI APIトークンの範囲を特定のプロジェクトとバージョンに限定し、GitHub Actionsワークフローで外部入力(ブランチ名、PRタイトルなど)を検証する必要があります。また、Trusted Publisherを使えばトークンの窃取そのものを防げます。
多層防御戦略
┌──────────────────────────────────────────────────────────────┐
│ PyPI サプライチェーン多層防御 │
├──────────┬──────────────┬──────────────┬─────────────────────┤
│ Layer 1 │ Layer 2 │ Layer 3 │ Layer 4 │
│ │ │ │ │
│ 依存関係 │ 脆弱性 │ ビルド環境 │ ランタイム │
│ 管理 │ スキャン │ セキュリティ │ 監視 │
│ │ │ │ │
│ Lockfile │ pip-audit │ Trusted │ SBOM │
│ Pinning │ safety │ Publisher │ 追跡 │
│ │ │ │ │
│ Hash │ GitHub │ PEP 740 │ 依存関係 │
│ 検証 │ Dependabot │ Attestation │ 監査 │
│ │ │ │ │
│ Private │ Snyk / │ 2FA / │ 異常検知 │
│ Index │ Socket.dev │ OIDC │ │
└──────────┴──────────────┴──────────────┴─────────────────────┘
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-audit | Safety CLI | Snyk |
|---|---|---|---|
| 脆弱性DB | PyPI 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.pyのinstallフックに悪意あるコードを挿入します。パッケージのインストール前に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']
セキュリティチェックリスト
プロジェクトにすぐ適用できるサプライチェーンセキュリティのチェックリストです。
依存関係の管理
- すべての依存関係が正確なバージョンで固定されているか(
==を使用) - requirements.txtまたはlockfileにハッシュが含まれているか(
--hash) - プライベートパッケージについて内部インデックスが優先設定されているか
- 使用していない依存関係が削除されているか
- 依存関係の更新周期が定義されているか(Dependabot/Renovate)
CI/CDのセキュリティ
- pip-auditまたはSafetyがCIパイプラインに統合されているか
- CRITICAL/HIGHの脆弱性が見つかったときにビルドが失敗するよう設定されているか
- SBOMがビルドごとに自動生成されるか
- GitHub ActionsでTrusted Publisherを使用しているか
- PyPI APIトークンの範囲が最小権限に限定されているか
アカウントのセキュリティ
- PyPIアカウントで2FA(二要素認証)が有効になっているか
- PyPI APIトークンがプロジェクトごとに分離されているか
- APIトークンがコードリポジトリにハードコードされていないか
- GitHub Actions secretsが安全に管理されているか
モニタリング
- 依存関係の変更に対する通知が設定されているか
- SBOMベースの脆弱性モニタリングが運用されているか
- 新しいCVEの発表時に影響分析のプロセスがあるか
結論
PyPIサプライチェーン攻撃は、単一のツールや単一のポリシーでは防げません。依存関係のロック(Lockfile Pinning + Hash Verification)、脆弱性スキャン(pip-audit + Safety)、ビルド環境のセキュリティ(Trusted Publisher + PEP 740 Attestation)、ランタイムのモニタリング(SBOM追跡)を組み合わせた多層防御戦略が必要です。
特に2026年現在で注目すべき3つの中核的な施策は次のとおりです。
- Trusted Publisherへの移行: PyPI APIトークンの代わりにOIDCベースのTrusted Publisherを使い、認証情報が奪われるリスクを根本から取り除く
- PEP 740アテステーションの活用: パッケージの出所を暗号学的に検証し、改ざんの有無を確認する
- SBOMの自動化: ビルドごとにSBOMを生成し、継続的に脆弱性モニタリングを実施する
サプライチェーンセキュリティは一度設定して終わりではなく、継続的に更新し監視しなければならない運用プロセスです。今日さっそく上のチェックリストをもとに、チームのセキュリティ状態を点検してみてください。
参考資料
- PyPI 2025年の年次レビュー - PyPI公式ブログ
- PyPIのデジタルアテステーション対応 - PyPI公式ブログ
- PEP 740 - デジタルアテステーションのためのインデックス対応 - Python PEP
- dYdXのnpm/PyPIパッケージのサプライチェーン攻撃 - The Hacker News
- Ultralyticsサプライチェーン攻撃の分析 - PyPI公式ブログ
- Ultralyticsサプライチェーン攻撃の防御方法 - Legit Security
- pip-audit - Python環境の脆弱性監査ツール - PyPA GitHub
- Safety CLI - Python依存関係の脆弱性スキャナー - PyPI
- CycloneDX Python SBOM生成ツール - CycloneDX GitHub
- サプライチェーン攻撃は我々の前提を悪用する - Trail of Bits Blog
- アテステーション: PyPIにおける新世代の署名 - Trail of Bits Blog
- Are we PEP 740 yet? - Trail of Bits
- SigstoreベースのPyPIアテステーションGA - Sigstore Blog