依存パッケージ — 自分が書いていないコードが、本番で動く
直接入れたのは40個。実際に入っていたのは900個。そのうち1つのメンテナのアカウントが乗っ取られ、CI の資格情報が外部へ送られた。
この記事の進み方
What — 入れた数と、動く数は違う
package.json に書く依存は「直接の依存」です。その各々にも依存があり、その依存にも依存があります(推移的な依存)。
直接の依存 40 個 ← 自分で選んだ
実際に入るもの 900 個 ← 選んでいない。全部が本番で動く
選んでいない 860 個も、自分のコードと同じ権限で動きます。ファイルを読み、ネットワークに出て、環境変数を読めます。「そのパッケージを信頼するか」は、その作者を信頼するかであり、さらにその作者が依存している人たちを信頼するかでもあります。
- 1依存を足すnpm install some-lib軽い
- 2バージョンを解決する^1.4.0 が 1.4.3 になる中
- 3インストール時のスクリプトpostinstall が走る重い
- 4ビルドに取り込まれるバンドルに混ざる重い
- 5本番で動く自分のコードと同じ権限重い
冒頭の事故は 3(インストール時のスクリプト)。実行すらしていないコードから、資格情報が漏れた。
危ないのは「実行されるまで」ではない。取り込んだ時点から、インストール時・ビルド時・実行時の3回、任意のコードが動く機会がある。
- 推移的な依存
- 自分が入れた依存が、さらに依存しているパッケージ。数はこちらのほうが桁違いに多い。
- lockfile
- 実際に入れたバージョンを固定して記録するファイル。package-lock.json など。
- インストール時スクリプト
- パッケージのインストール中に実行されるコマンド。postinstall など。
- セマンティックバージョニング
- メジャー・マイナー・パッチで互換性を表す約束。守るかどうかは作者しだい。
範囲指定と lockfile
^1.4.0 は「1.x.x の範囲で、新しいものを使ってよい」という指定です。便利さと引き換えに、何が入るかを毎回ゆだねています。
新しいパッチ版が、黙って入る
"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 が公開されていたら何が入りますか?
Why — なぜ「使っていないコード」から漏れるのか
冒頭の事故で、そのユーティリティの機能は一度も呼ばれていません。それでも資格情報は漏れました。
インストールの時点で、コードが実行されるからです。
多くのパッケージ管理の仕組みには、インストール中に任意のコマンドを走らせる機能があります(postinstall など)。ネイティブのビルドや初期設定のための正当な機能ですが、任意のコマンドである以上、何でもできます。
npm ci
→ 依存を展開
→ 各パッケージの postinstall を実行 ← ここ。自分のコードは1行も動いていない
→ 完了
そして CI の環境には、たいてい強い権限があります。デプロイ用の資格情報、レジストリのトークン、署名鍵。開発者の手元の端末にも、クラウドの認証情報があります。
気づきにくい理由
このたぐいの事故は、動作としては何も壊れません。テストは通り、ビルドも通り、アプリも正常に動きます。壊れないので、CI の結果からは何も分かりません。
気づく経路は限られます。
- 請求やリソースの異常(冒頭はこれ。4日後)
- 依存の脆弱性を知らせる仕組み(公表されてから)
- 通信の監視(普段と違う送信先)
「壊れないこと」を前提に備えるのが、この領域の考え方です。
依存パッケージの postinstall スクリプトは、CI の環境で何ができますか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
CI での依存の取り込み方を、どう設計しますか?
- CI はデプロイ用の資格情報を環境変数で持っている
- 直接の依存は40個、入るのは900個以上
- 依存の中には、ネイティブのビルドが必要なものが1つある
- 脆弱性の修正は、なるべく早く取り込みたい
- 1日に数回ビルドが走る
- · 毎回同じものが入ることを、どう保証するか
- · インストール時にコードが走ることを、どう扱うか
- · 資格情報が、いつ・どこに存在しているか
「依存を1つ足すだけです」と言う同僚に、何を一緒に考えたいかを伝えてください
- 足したいのは、文字列を整形する小さなパッケージ(自分で書けば30行ほど)
- 相手は締め切りが近く、早く進めたい
- 依存を足すこと自体を禁止したいわけではない
- チームは5人で、このコードを数年運用する見込み
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。