仕組みから学ぶ Web
応用読了目安 15#依存関係#サプライチェーン#lockfile#postinstall#npm

依存パッケージ — 自分が書いていないコードが、本番で動く

直接入れたのは40個。実際に入っていたのは900個。そのうち1つのメンテナのアカウントが乗っ取られ、CI の資格情報が外部へ送られた。

この記事の進み方

What — 入れた数と、動く数は違う

package.json に書く依存は「直接の依存」です。その各々にも依存があり、その依存にも依存があります(推移的な依存)。

直接の依存         40 個   ← 自分で選んだ
実際に入るもの    900 個   ← 選んでいない。全部が本番で動く

選んでいない 860 個も、自分のコードと同じ権限で動きます。ファイルを読み、ネットワークに出て、環境変数を読めます。「そのパッケージを信頼するか」は、その作者を信頼するかであり、さらにその作者が依存している人たちを信頼するかでもあります。

Figure依存が本番で動くまで
  1. 1依存を足すnpm install some-lib軽い
  2. 2バージョンを解決する^1.4.0 が 1.4.3 になる
  3. 3インストール時のスクリプトpostinstall が走る重い
  4. 4ビルドに取り込まれるバンドルに混ざる重い
  5. 5本番で動く自分のコードと同じ権限重い

冒頭の事故は 3(インストール時のスクリプト)。実行すらしていないコードから、資格情報が漏れた。

1/5
依存を足す自分で選ぶのは1つでも、その依存がまとめて入る。何が増えるかは、入れてみるまで分からないことが多い。

危ないのは「実行されるまで」ではない。取り込んだ時点から、インストール時・ビルド時・実行時の3回、任意のコードが動く機会がある。

推移的な依存
自分が入れた依存が、さらに依存しているパッケージ。数はこちらのほうが桁違いに多い。
lockfile
実際に入れたバージョンを固定して記録するファイル。package-lock.json など。
インストール時スクリプト
パッケージのインストール中に実行されるコマンド。postinstall など。
セマンティックバージョニング
メジャー・マイナー・パッチで互換性を表す約束。守るかどうかは作者しだい。

範囲指定と lockfile

^1.4.0 は「1.x.x の範囲で、新しいものを使ってよい」という指定です。便利さと引き換えに、何が入るかを毎回ゆだねています。

Compareバージョンの指定と、入るもの

新しいパッチ版が、黙って入る

"date-lib": "^1.4.0" → npm install
再現性
**実行するたびに変わりうる**
修正の取り込み
自動で入る
乗っ取られた版
**自動で入る**
気づけるか
差分が残らないと気づけない
推移的な依存
更新の手間
  • 冒頭の事故の設定。CI が毎回 npm install していた
  • npm install は lockfile を更新しうる。CI では差分が捨てられる
  • 「最新の修正が入る」は利点でもあり、そのまま危険でもある

lockfile があるかどうかと、どのコマンドを使うかで、入るものが変わる。CI で「毎回同じものが入る」を保証できているかが分かれ目。

確認 — ここまで読めたか

package.json に「date-lib: ^1.4.0」と書き、package-lock.json には 1.4.2 が記録されています。CI で npm install を実行したとき、乗っ取られた 1.4.3 が公開されていたら何が入りますか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜ「使っていないコード」から漏れるのか

冒頭の事故で、そのユーティリティの機能は一度も呼ばれていません。それでも資格情報は漏れました。

インストールの時点で、コードが実行されるからです。

多くのパッケージ管理の仕組みには、インストール中に任意のコマンドを走らせる機能があります(postinstall など)。ネイティブのビルドや初期設定のための正当な機能ですが、任意のコマンドである以上、何でもできます。

npm ci
  → 依存を展開
  → 各パッケージの postinstall を実行   ← ここ。自分のコードは1行も動いていない
  → 完了

そして CI の環境には、たいてい強い権限があります。デプロイ用の資格情報、レジストリのトークン、署名鍵。開発者の手元の端末にも、クラウドの認証情報があります。

気づきにくい理由

このたぐいの事故は、動作としては何も壊れません。テストは通り、ビルドも通り、アプリも正常に動きます。壊れないので、CI の結果からは何も分かりません。

気づく経路は限られます。

  • 請求やリソースの異常(冒頭はこれ。4日後)
  • 依存の脆弱性を知らせる仕組み(公表されてから)
  • 通信の監視(普段と違う送信先)

「壊れないこと」を前提に備えるのが、この領域の考え方です。

確認 — ここまで読めたか

依存パッケージの postinstall スクリプトは、CI の環境で何ができますか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

CI での依存の取り込み方を、どう設計しますか?

与えられた条件
  • CI はデプロイ用の資格情報を環境変数で持っている
  • 直接の依存は40個、入るのは900個以上
  • 依存の中には、ネイティブのビルドが必要なものが1つある
  • 脆弱性の修正は、なるべく早く取り込みたい
  • 1日に数回ビルドが走る
この軸で考える
  • · 毎回同じものが入ることを、どう保証するか
  • · インストール時にコードが走ることを、どう扱うか
  • · 資格情報が、いつ・どこに存在しているか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「依存を1つ足すだけです」と言う同僚に、何を一緒に考えたいかを伝えてください

与えられた条件
  • 足したいのは、文字列を整形する小さなパッケージ(自分で書けば30行ほど)
  • 相手は締め切りが近く、早く進めたい
  • 依存を足すこと自体を禁止したいわけではない
  • チームは5人で、このコードを数年運用する見込み

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。