AetherEchoesEngineering
Engineering#008813 min2,40935 view

npm/PyPI のサプライチェーン攻撃を、個人開発で踏まないために私がやっていること

Kevin Patel の風刺記事『No Way To Prevent This』が r/programming 1 位になっていた。読みながら、自分が npm と PyPI でどんな防衛策を積んできたかを書き出した。依存を増やさない / lockfile を読む / postinstall を切る / token を絞る / PR 経由で更新する、の 5 本立てです。

SoSoraEndo2026年5月18日 12:0913 min2,409

なぜ今これを書いているか

npm と PyPI のサプライチェーン攻撃は、構造的に 繰り返される類のインシデント です。完全に防ぐ手段はありませんが、踏む確率と踏んだ時の損害は、個人開発レベルでもかなり下げられます。私が実際にやっている 5 つの対策を順に書きます。

2026 年 5 月 17 日、Reddit の r/programming で Kevin Patel の風刺記事 No Way To Prevent This, Says Only Package Manager Where This Regularly Happens が 1372 score の 1 位になっていました。タイトルが既に強い。アメリカの銃乱射事件のたびに繰り返される The Onion の風刺見出しをもじったもので、npm の事故が「またか」で消費される構造を皮肉っています。

読みながら自分の手元の package-lock.jsonuv.lock を開いて、何をしてきたかを思い出していました。完璧な防御は無理でも、普段やる小さい習慣の積み重ね で、踏んだときの傷を浅くすることはできます。以下は私の個人的な運用記録で、Best Practice ではありません。

1. 依存を増やさないが、一番きく

第一防衛は 依存を増やさないこと。これに尽きます。コードを書く速度を多少落としてでも、新しい dependency を入れる前に 30 秒考える癖が、結果的に一番効きました。

私の判断基準は 3 つです:

  • そのライブラリを抜くと、自分のコードが何行増えるか。50 行で書けるなら依存にしない
  • npm/PyPI の weekly download が極端に少ない(npm で週 1 万未満)なら避ける
  • 最終更新が 2 年以上前なら、フォーク前提で読みに行く

この 3 つを通った上で、npm view <pkg> maintainers で誰がメンテしているかを 1 度だけ見ます。GitHub の組織名と一致しないアカウントが publish 権限を持っていたら、それは赤信号ではないけれど黄信号。issue の閉じ方の温度感も見ます。

私の AetherEchoes バックエンド(Rails 8.1 / Ruby 3.4.9)は Gemfile 28 行で、フロントエンド(Next.js 16 / TypeScript 6)は package.jsondependencies が 14 個、devDependencies が 12 個です。少ない方だと思います。lodashmoment も入っていません。標準ライブラリで足りる範囲は標準で書く。これは速度を犠牲にしますが、lock ファイルを開いて読める量に保つため の意図的な選択です。

2. lockfile を「読む」習慣をつける

package-lock.jsonuv.lock読むものです。CI が通ったから OK、ではなく、PR で diff を目で追う。これが第二防衛です。

私の場合、npm install <pkg> で 1 行追加した時に、lockfile の diff が +800 行になることがあります。そういう時は一旦止まって、何が連鎖して入ってきたかを npm ls <transitive_pkg> で辿ります。たとえば先月、Next.js の関係で glob を更新したら、fsevents 経由で macOS-only バイナリが入ってきました。これ自体は問題ない依存ですが、diff が予想外に大きい時は、確実に何かを理解していないサイン です。

PyPI 側は uv.lock をプロジェクトに置いていて、uv pip compile の diff を Git に通します。requirements.txt だけだとバージョンの上限下限が曖昧になるので、必ず lock を切る。これは PyPI の package タイポスクワッティング(例: requestsrequest で publish するやつ)が、lockfile を経由しない素の pip install で踏みやすいからです。

余談ですが、JavaScript 系で package-lock.json を Git に入れないリポジトリを今でも見かけます。これは私には信じられない運用で、CI が壊れる原因の半分は lock の不一致なので、入れない理由は無いと思っています。

3. postinstall を無効化する習慣

npm の postinstall スクリプトは、依存の package が npm install 時に 任意のコードを実行できる仕組み です。サプライチェーン攻撃の主要な経路で、ここを切るだけでも相当楽になります。

私は手元の ~/.npmrc に下記を書いています:

ignore-scripts=true

これで npm install 時に postinstall が走らなくなります。困るのは sharpnode-sass のような native build が必要な依存 で、これらは npm install 後に明示的に npm rebuild sharp を打ちます。手間は増えますが、知らない依存が node_modules/.bin に何かを生やすリスクはゼロになります。

プロジェクト単位で切り替えたい場合は package.json の隣に .npmrc を置く方法もあります。CI では npm ci --ignore-scripts を default にしておくと、開発機との挙動差が無くなります。

PyPI 側にも pyproject.toml の build hook(hatchlingsetuptoolssetup.py)で同様のことが起きます。pip install --no-build-isolationuv pip install --no-build-isolation も検討に値しますが、ここは私もまだ運用が固まっていません。一律で切ると cryptography のような大きい依存で困ります。「困ったら都度許可する」くらいの粒度です。

4. publish token は publish 専用に絞る

自分が npm/PyPI に publish 側として関わっている場合、token を読み取りも publish もできる強い権限のまま使わない。これは攻撃を受けた時に被害が広がる経路を塞ぐ話です。

npm の場合、Automation token を作って、特定パッケージへの publish スコープだけに絞ります。https://www.npmjs.com/settings/<user>/tokensGranular Access Token を選び、有効期限を 90 日に切る。CI には secrets として渡し、ローカルにコピーしない。これは、もし CI のログが漏れたとしても被害がそのパッケージだけに留まるようにするためです。

PyPI の場合、Project-scoped API token を発行します。アカウント全体の token は 絶対に CI に置かないpypi.org/manage/account/token/ で対象 project を指定して発行します。期限は PyPI 側で切れないので、自分で 90 日のリマインダを Calendar に入れて手で rotate しています。

運用してみて学んだことが 1 つ。token の rotate 作業は面倒だが、面倒だからこそ「自分が publish しているパッケージの本数」を意識する ようになります。これは結果的に、メンテできない数を抱え込まないための歯止めにもなっています。私の場合、3 つ以上の package を同時にメンテすると確実に rotate を忘れるので、自分の publish 本数は 2 つを上限にしました。

5. PR 経由でしか更新しない

依存の更新は 必ず PR を経由します。手元で npm update を叩いて main にコミット、は禁止です。これは Dependabot か Renovate の出番です。

私の AetherEchoes は GitHub Dependabot を .github/dependabot.ymlweekly で動かしています。毎週月曜に PR が立ち、私は CI のグリーンを見て手動で merge します。自動 merge はしません。なぜなら、CI が green でも package-lock.json の diff が直感に反することがあるからです。

Dependabot を入れる時の落とし穴を 1 つ書いておきます。open-pull-requests-limit の default は 5 で、これだと minor / patch の更新が積もって PR が詰まります。私は 3 に下げて、優先度の高い security 更新だけが上に来るようにしました。groups 設定で rails-related のように関連する gem をまとめると、レビュー回数も減ります。

6. それでも踏んだ時のための、最小の備え

ここまでの 5 つをやっていても、踏む時は踏みます。Kevin Patel の風刺記事の通りです。だから踏んだ後にどう動くかを、平時に書いておく価値があります。

私が手元の Notion に置いている incident response メモは 4 行だけです:

  1. 該当 package の version を Git history で特定 → git log --oneline package-lock.json
  2. その version より前の commit hash を保存 → 戻し先
  3. 影響範囲を考える(token が漏れた可能性 → 即 rotate / 機密データが読まれた可能性 → 監査ログ確認)
  4. 1 と 2 から git revert ではなく lockfile の 手動編集 + npm ci で戻す

3 がいちばん重い。実害があったかは事後にしか分からないので、「あったと仮定して動く」 のが基本です。token は rotate、PostgreSQL の admin password も念のため変える、API key の漏洩可能性があるなら public log にも grep をかける。面倒ですが、踏んだ事実が確定した時点では「本当に被害が出たか」より「被害が出ていない前提で動かないこと」の方が大事です。

まとめ — できることだけやる

書き出してみると、どれも特別なテクニックではありませんでした。依存を増やさず、lockfile を読み、postinstall を切り、token を絞り、PR 経由で更新する。これだけです。

Kevin Patel の風刺記事のタイトルが言う通り、構造的にはこれからも事故は起きます。私たちにできるのは、起きた時に自分が 「最近の依存追加を覚えている人」 であり続けることくらいです。npm install <思いつき> を 1 行減らすだけで、その「覚えている」距離は確実に縮みます。

以前 Rails 6.1 → 8.1 のメジャー移行で踏んだ 7 つの罠 でも依存更新の話を書きましたが、メジャー移行も結局はこの 5 本立ての応用です。lockfile を読む筋肉は、平時の小さい更新で鍛えるしかありません。

Tags

参考文献

  1. No Way To Prevent This, Says Only Package Manager Where This Regularly Happens — Kevin Patel
  2. npm CLI — ignore-scripts config
  3. PyPI — API Tokens

Reaction

Share

X (Twitter)