npmとpipのサプライチェーンセキュリティ:依存関係の混同とタイポスクワッティング攻撃の検出

Security tutorial - IT technology blog
Security tutorial - IT technology blog

クイックスタート:5分でプロジェクトをスキャンする

今すぐ現在のプロジェクトに対して実行してみましょう。Pythonにはpip-audit、Nodeにはnpm auditが必要ですが、どちらも追加設定なしでほとんどの環境で動作します。

# Python:インストール済みパッケージの監査
pip install pip-audit
pip-audit

# Node.js:npm依存関係の監査
npm audit
npm audit --json | jq '.vulnerabilities | keys'

highcriticalとしてフラグが立っているものがあれば、デプロイ前に必ず止めてください。これはベースラインチェックですが、既知のCVEしか検出できません。依存関係の混同とタイポスクワッティングはまったく異なる種類の攻撃であり、標準的な監査では見逃してしまいます。このガイドはそのギャップを埋めるためのものです。

これらの攻撃で実際に何が起きているのか

依存関係の混同(名前空間の混同)

仕組みはシンプルかつ凶悪です。あなたの会社がプライベートなnpmレジストリやPyPIミラー上に内部パッケージ(例:acme-utils)をホストしているとします。攻撃者が公開レジストリに同じ名前のパッケージを公開し、バージョン番号だけ高く設定します。CI/CDパイプラインがnpm installpip installを実行すると、パッケージマネージャーは公開レジストリのものを取得します。バージョンが高い方が勝つ——それだけです。

Alex Birsanは2021年にこの手法を実証し、単一の研究開示でMicrosoft、Apple、Shopifyをはじめとする32社からバグバウンティを獲得しました。ペイロードは自動的に実行されました——ユーザー操作も特別な権限も不要。ただのnpm installで。

タイポスクワッティング

誰かがrequestsの代わりにrequetscross-envの代わりにcros-envを登録し、待ち続けます。開発者はミスタイプをします。CIスクリプトはエラーごとコピペされます。締め切りのプレッシャーで人は粗雑になる——依存関係名の1文字違いだけで十分なのです。

colouramacoloramaのタイポ)はPyPIに削除されるまでに500回以上ダウンロードされました。悪意あるペイロードは、npmのpostinstallスクリプトやpipのsetup.pyを通じてインストール時に実行されます——コード内でパッケージをインポートする前に。YARAルールを使ったLinuxマルウェアハンティングはこうした悪意あるスクリプトの痕跡を事後検知する際にも有効です。

詳細解説:検出テクニック

difflibとカスタムスクリプトによるタイポスクワッティングの検出

ファジーマッチングを使ってrequirements.txtを既知の正規パッケージリストと照合します。本物のパッケージ名と0.85以上の類似度があるのに、ホワイトリストにないものはすべてフラグを立てます。

import difflib

# 既知の正規パッケージ(定期的に更新すること)
KNOWN_PACKAGES = [
    "requests", "flask", "django", "numpy", "pandas",
    "boto3", "fastapi", "pydantic", "sqlalchemy", "celery"
]

def check_typosquatting(package_name, threshold=0.85):
    matches = difflib.get_close_matches(
        package_name,
        KNOWN_PACKAGES,
        n=3,
        cutoff=threshold
    )
    if matches and package_name not in KNOWN_PACKAGES:
        print(f"[警告] '{package_name}' は以下に類似しています: {matches}")
        return True
    return False

# requirements.txt から読み込む
with open("requirements.txt") as f:
    for line in f:
        pkg = line.strip().split("==")[0].split(">")[0].split("<")[0]
        if pkg:
            check_typosquatting(pkg)

完璧ではありませんが、明らかなケースを検出する安価な第一段階として機能します。CIパイプラインに組み込んでおけば、初めて検知した時点でコストを回収できます。

pip-auditとSafetyによる既知の悪意あるパッケージの検出

# pip-auditはPyPI Advisory Databaseに対してチェックする
pip-audit -r requirements.txt --fix

# Safetyはキュレートされた脆弱性データベースに対してチェックする
pip install safety
safety check -r requirements.txt --full-report

npm:不審なpostinstallスクリプトの検出

悪意あるnpmパッケージのほぼすべてが、インストール時にコードを実行するためにライフサイクルスクリプトを使用します。実行される前にパッケージが何を宣言しているか確認しましょう:

# インストール前にライフサイクルスクリプトを確認
npm pack some-package --dry-run
npx can-i-ignore-scripts some-package

# スクリプトを無効にしてインストール(一部の正規パッケージが動作しなくなるが、監査には安全)
npm install --ignore-scripts

# インストール済みパッケージのpostinstallフックをスキャン
cat node_modules/*/package.json | jq -r 'select(.scripts.postinstall) | "\(.name): \(.scripts.postinstall)"'

パッケージメタデータの危険なシグナルを確認する

# npm:公開日、メンテナー数、ダウンロード統計を確認
npx npm-package-stats some-package

# PyPI:API経由でメタデータを取得
curl -s https://pypi.org/pypi/requests/json | jq '{
  author: .info.author,
  maintainers: .info.maintainer,
  home_page: .info.home_page,
  upload_time: .urls[0].upload_time
}'

昨日公開された。GitHubリンクなし。メンテナー情報なし。名前は内部ツールと完全に一致。これが依存関係の混同ペイロードの典型的な特徴です——一度に4つのレッドフラグが揃っています。

応用編:サプライチェーンを強固にする

スコープパッケージとレジストリ設定(npm)

内部パッケージを組織名でスコープし、そのスコープをプライベートレジストリのみから解決するようnpmに指示します:

# プロジェクトルートの .npmrc
@mycompany:registry=https://npm.mycompany.internal
always-auth=true

この設定により、npmは@mycompany/*パッケージに対して公開レジストリに一切アクセスしません。依存関係の混同はコード変更不要で設定レベルで無効化されます。

pipのインデックス優先度

--index-urlでプライベートレジストリを主要ソースとして設定し、公開パッケージのフォールバックとして--extra-index-urlを使用します:

# pip.conf — プライベートレジストリを優先
[global]
index-url = https://pypi.mycompany.internal/simple/
extra-index-url = https://pypi.org/simple/

より安全なパターンは、内部パッケージにextra-index-urlを一切使わないことです。直接ベンダリングするか、PyPIをミラーリングしてアローリストを強制できるNexusやArtifactoryのようなレジストリプロキシを使用してください。これにより、同じパッケージ名が両方のレジストリに存在する場合のpipの曖昧さを排除できます。

ロックファイルは必須

ロックファイルは必ずコミットしてください。例外なし。

# Node.js — package-lock.json または yarn.lock をコミット
git add package-lock.json

# Python — バージョン固定された requirements ファイルを生成してコミット
pip freeze > requirements.lock
git add requirements.lock

# CIはrequirements.txtではなくロックファイルからインストールする
pip install -r requirements.lock
npm ci  # npm install ではなく

npm ciがここでの重要な違いです。package-lock.jsonに記載された内容を正確にインストールし、不一致があれば新しいバージョンに暗黙的に解決するのではなく、ハードエラーで失敗します。

ハッシュでパッケージの整合性を検証する

# pip:ハッシュ検証を必須にする
pip install --require-hashes -r requirements.txt

# pip-compileでハッシュ付きrequirementsを生成(pip-toolsより)
pip install pip-tools
pip-compile --generate-hashes requirements.in

ハッシュ固定されたrequirements.txtはこのような形式になります:

requests==2.31.0 \
    --hash=sha256:58cd2187423d77b8d5e82b788 \
    --hash=sha256:942c5a758f98d790eaed1a29c

ダウンロードしたパッケージがリストされたハッシュ値と一致しない場合、pipはインストールを拒否します。これにより、改ざんされたパッケージとレジストリ自体へのMITM攻撃の両方をカバーできます。

実際の運用から得た実践的なヒント

CI/CDでの自動スキャン

GitHub ActionsやGitLab CIのパイプラインステップとして組み込みましょう——30秒以内で実行でき、本番環境に到達する前に問題を検出します。TrivyをCI/CDに組み込んでDockerイメージのCVEもスキャンすれば、依存関係とコンテナイメージ両方を同一パイプラインでカバーできます:

# .github/workflows/security.yml
jobs:
  dependency-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Pythonの監査
        run: |
          pip install pip-audit
          pip-audit -r requirements.txt
      
      - name: npmの監査
        run: npm audit --audit-level=high
      
      - name: 不審なインストールスクリプトの確認
        run: |
          cat node_modules/*/package.json 2>/dev/null | \
          python3 -c "
import sys, json
for line in sys.stdin:
    try:
        pkg = json.loads(line)
        scripts = pkg.get('scripts', {})
        if any(k in scripts for k in ['preinstall','install','postinstall']):
            print(f\"{pkg.get('name')}: {scripts}\")
    except: pass
"

名前空間の予約

内部パッケージがスコープされていない場合は、公開レジストリで自分でその名前を登録してください。明確な警告READMEを持つ空のプレースホルダーパッケージを公開します。5分の手間で永続的にスクワッティングを防止できます。

# PyPIにプレースホルダーを登録する
# 最小限のsetup.pyを作成し、READMEに大きな警告を追加する
# これは内部パッケージであり、公開バージョンはプレースホルダーであることを明記する
python setup.py sdist upload

プルリクエストでの依存関係レビュー

GitHubのDependency Review Actionは、脆弱性があるまたは新たに追加された高リスクな依存関係を導入するPRを、コードがマージされる前にブロックします:

- uses: actions/dependency-review-action@v4
  with:
    fail-on-severity: high
    deny-licenses: GPL-2.0, AGPL-3.0

レジストリの認証情報を強固に保つ

プライベートレジストリには認証が必要であり、その認証情報は攻撃対象になります。定期的にローテーションし、適切に生成してください——32文字以上、フルキャラクターセットで。レジストリトークンにはtoolcraft.app/ja/tools/security/password-generatorを使用しています:ブラウザ内で完全に動作するため、ネットワークリクエストには何も送信されません。

内部パッケージ名に一致する新規パッケージ登録を監視する

socket.devのようなサービスはnpmをリアルタイムで監視し、定義したパターンに一致する新しいパッケージが現れた際にアラートを送信します。内部パッケージ名のアラートを設定しておきましょう——数日後ではなく、数分以内に把握したいものです。

# socket.dev CLIの統合
npm install -g @socketsecurity/cli
socket scan .

ロックファイルを使う。内部パッケージをスコープする。可能な限りスクリプトを無効にする。CIでスキャンする。この4つの習慣で攻撃面の大半を排除できます——専任のセキュリティチームがいなくても実践できます。サーバー全体のセキュリティ状態を定期的に確認したいなら、Lynisによる自動セキュリティ監査と組み合わせると包括的な防御ラインを構築できます。

Share: