is-promiseで何が起きたのか、そこから何を学べるのか
2020年4月28日
0 分で読めます2020年4月25日、JavaScript開発者でメンテナーのis-promiseライブラリのバージョン2.2.0が、Forbes Lindesay氏によってnpmにリリースされました。報告によると、このリリースにより、Facebookのcreate-react-appやGoogleのfirebase-tools、angular-cli,など、新しいプロジェクトのひな型作成に使われる人気の開発者向けビルドツールで障害が発生しました。Forbes氏は2.2.0に関連する問題に迅速に対処し、最終的に約3時間後に2.2.2をリリースしました。
またleft-padの再来?いえ、そうではありません。
この話を共有する人々の間で広まっている見方とは異なり、これはソフトウェアサプライチェーンの問題ではなく、2016年のleft-padのインシデントで起きたこととも異なります。
さらに、この問題の根本にあったのは、誰にでも起こり得る悪意のないミスでした。Forbes氏は責任あるメンテナーとして、インシデント発生から数時間も経たないうちに修正を提供しました。また、このインシデントの事後分析記事も執筆しており、ぜひ読んでみることをおすすめします。
この記事では、is-promiseのインシデントに至った経緯と、メンテナーやユーザーとしてそこから何を学べるのかを説明します。
is-promiseのインシデントは誰に影響したのか?
npmパッケージのis-promiseは約500の直接依存パッケージで使われ、週に1,200万回ダウンロードされています。人気のパッケージと言って差し支えないでしょう。ただし、チームのビルドを何千件も壊したわけではありません。主に、開発者ツールを使って新しいプロジェクトを作成したユーザーに影響しました。
直接依存パッケージが500というと、それほど人気ではないように聞こえるかもしれません。しかし、npmのエコシステムでは依存関係が複雑にネストしていることを考えると、GitHubの統計にある「350万のプロジェクトがis-promiseに依存」という数字から、人気のほどがよりよく分かるでしょう。
さらに、問題が発生したのはNode.js 12.16以降(LTSリリース)を使っている場合に限られます。GitHubで見かけた報告の多く(例:[1]、[2])では、Node.js 13または14のユーザーが挙げられています。これらは安定版ではなく、最新の機能をいち早く試したい場合を除き、推奨されていません。
実際には何が起きたのでしょうか?
メンテナーはライブラリにES Modules(ESM)のサポートを追加し、Node.jsで長年使われてきたCommonJS(CJS)モジュールシステムと、比較的新しい標準化されたJavaScriptモジュールシステムの両方を同時に利用できるようにしようとしました。
これは5年ぶりのリリースとなる2.1.0で行われました。以下に差分を示します。(GitHubでis-promiseの元の比較を見る)更新内容を簡単にまとめると、次のとおりです。
明示的なデフォルトエクスポートの追加
package.jsonでexportsキーを使ってデュアルモジュールシステムを指定する比較的新しい方法を採用し、ESMのtypeキーも定義TypeScriptの型定義を更新

では、実際に何が問題だったのでしょうか?exportsフィールドの定義が不適切だったため、次の例外が発生し、GitHubでユーザーが問題を報告することになりました。
このインシデントから得られる教訓は?
今回のようなインシデントに今後より適切に備えるため、メンテナーとユーザーの双方に学ぶべきことがあると思います。実際、Forbes氏はすでにメンテナーとしていくつかのチェック機能を追加し、同様の問題を未然に防げるようにしています。
これはソフトウェアサプライチェーンの問題なのか?
この誠実なミスは誰にでも起こり得るものであり、is-promiseがわずか1行のライブラリであることに起因するものでも、ソフトウェアサプライチェーンそのものの問題でもありません。
left-padのインシデントでは、npmレジストリにおけるパッケージの所有権やライフサイクルなどの管理の弱点が明らかになりました。その結果、同様の事態を防ぐためのポリシーや対策が導入されました。では、npmレジストリはis-promiseの問題を解決するために何をしたのでしょうか?何もしていません。必要がなかったからです。すでに述べたとおり、これはソフトウェアサプライチェーンの問題ではありません。
セマンティックバージョニングは重要
package.jsonのtypeキーとexportsキーを使ってCommonJSからESMへのサポートを追加または切り替えることは、互換性を損なう変更として扱う必要があるとドキュメントに明記されています。しかし、is-promise@2.2.0はマイナー変更として公開され、多くの依存パッケージで最新バージョンとして解決されてしまいました。
したがって、2つ目の教訓は、セマンティックバージョニングを慎重に考慮することです。エコシステムへの影響は大きいものです。
パッケージのエンドツーエンドテスト
リリース前にこの問題を発見する方法の1つは、利用者の視点でパッケージをテストするエンドツーエンドテストを追加し、想定どおりに動作することを確認する健全性テストを行うことです。
Forbes氏は最新バージョンにこれらのエンドツーエンドテストを追加しました。私も、Pie My Vulnsというパッケージで、少し異なるものの考え方は似ているエンドツーエンドテストを実施しています。CircleCIのテストでは、ローカルのパッケージレジストリとしてVerdaccioを起動してパッケージを公開し、npm installや、健全性を確認するAPIまたはCLIコマンドが想定どおりに動作することを確かめています。
Node.jsのLTSを使う
常にNode.jsのLTSを使うべきです。この記事の執筆時点では、バージョン12が該当します。Node.js 13や14のような最新バージョンを使っていた場合、間違いなくis-promiseの変更による問題に直面していたでしょう。
ただし、このケースでは必ずしもそうとは言えません。Node.js 12.16以降はESMの実験的サポートがフラグなしで有効になっていたため、問題の影響を受けました。12.15以前を使い続けていた場合、この問題の影響は受けませんでした。
ここからもう1つ教訓が得られます。Node.js 14などの後続バージョンへの対応を追加していれば問題を発見できた可能性はありますが、Node.jsのLTSバージョンですべてのテストが通ることを必ず確認しましょう。
アップグレードを急ぎすぎていませんか?
is-promise@2.2.0への自動アップグレードを提案するプルリクエストを受け取り、上記と同じ影響条件に該当していた場合、あなたもこの問題の影響を受ける可能性がありました。
今後は、こうした問題や悪意のあるパッケージを避けるためにも、急いでアップグレードせず、少し様子を見ることをおすすめします。そのため、Snykの自動アップグレードではバージョンの更新を急がず、新しいバージョンをプロジェクトに提案するまで21日間の猶予を設けています。
npxの仕組みを知っていますか?
報告された問題の多くでnpxの使用に言及があったため、誤解を解いておきたいと思います。ロックファイル、具体的にはpackage-lock.jsonやyarn.lockは、一般にパッケージと一緒に公開されないため、ここでは役に立ちません。仮に公開されていたとしても、npxの実行時に行われるインストールでは読み込まれません。
ただし、npmのshrinkwrapロックファイル(npm-shrinkwrap.json)を使えば、is-website-vulnerableで私が行ったように依存関係を固定できます。レジストリで新しいバージョンが公開されても、常に同じツリー、つまりテスト済みのものを出荷できるようになります。
ロックファイルの仕組みをもっと知りたい方は、npmエコシステムにおけるパッケージのロックファイルを理解する方法について書いた記事もご覧ください。
まとめ
最後に、こうした問題は起こり得るものです。コミュニティが力を合わせ、Forbes Lindesay氏が迅速に問題へ対処してくれたことに感謝しましょう。開発者やメンテナーである私たちにも、多くの学びがあります。私たちは皆、オープンソースという国の一員なのです。
もっと知りたい方は、Forbes氏の事後分析記事を読み、このインシデントから得た教訓をぜひご確認ください。
is-promiseの2.x系の修正版は2.2.2です。ES ModulesやTypeScriptのサポートを目的にアップグレードする場合は、新しいメジャーバージョンの4.0.0も利用できます。
CTFを始めよう
オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。


