メインコンテンツにスキップ
バージョン: 最新 (v5.0.x)

プロトタイプ汚染

以下は、Eran Hammer氏によって書かれた記事です。後世のためにここに再現されています(許可を得ています)。元のHTMLソースからMarkdownソースに再フォーマットされていますが、それ以外は同じです。元のHTMLは上記の許可リンクから取得できます。

プロトタイプ汚染の歴史

Eran Hammer氏の記事に基づくと、この問題はWebセキュリティのバグによって引き起こされます。また、オープンソースソフトウェアの保守に必要な労力と、既存のコミュニケーションチャネルの限界を明確に示しています。

しかし、まず、JavaScriptフレームワークを使用して受信JSONデータを処理する場合、プロトタイプ汚染全般、およびこの問題の具体的な技術的な詳細について読んでください。これは重大な問題になる可能性があるため、最初に独自のコードを確認する必要がある場合があります。特定のフレームワークに焦点を当てていますが、外部データを処理するためにJSON.parse()を使用するソリューションはすべて潜在的に危険にさらされています。

BOOM

Lob(長年の寛大なサポーター!)のエンジニアリングチームは、データ検証モジュールであるjoiで特定された重大なセキュリティ脆弱性を報告しました。彼らはいくつかの技術的な詳細と、提案された解決策を提供しました。

データ検証ライブラリの主な目的は、出力が定義されたルールに完全に準拠していることを確認することです。準拠していない場合、検証は失敗します。合格した場合、作業中のデータが安全であると盲目的に信頼できます。実際、ほとんどの開発者は、検証済みの入力をシステムの整合性の観点から完全に安全なものとして扱っており、これは非常に重要です!

私たちの場合、Lobチームは、検証ロジックによって一部のデータがエスケープされ、検出されずに通過できる例を提供しました。これは、検証ライブラリが発生する可能性のある最悪の欠陥です。

プロトタイプの概要

これを理解するには、JavaScriptの仕組みを少し理解する必要があります。JavaScriptのすべてのオブジェクトには、プロトタイプを持つことができます。これは、別のオブジェクトから「継承」するメソッドとプロパティのセットです。JavaScriptは実際にはオブジェクト指向言語ではないため、「継承」を引用符で囲んでいます。プロトタイプベースのオブジェクト指向言語です。

ずっと前に、無関係ないくつかの理由で、誰かが特別なプロパティ名__proto__を使用してオブジェクトのプロトタイプにアクセス(および設定)することを決定しました。これは非推奨になりましたが、完全にサポートされています。

デモ

> const a = { b: 5 };
> a.b;
5
> a.__proto__ = { c: 6 };
> a.c;
6
> a;
{ b: 5 }

オブジェクトにはcプロパティがありませんが、そのプロトタイプにはあります。オブジェクトを検証する場合、検証ライブラリはプロトタイプを無視し、オブジェクト独自のプロパティのみを検証します。これにより、cはプロトタイプを介して侵入できます。

もう1つの重要な部分は、JSON形式のテキストをオブジェクトに変換するために言語によって提供されるユーティリティであるJSON.parse()が、この魔法の__proto__プロパティ名を処理する方法です。

> const text = '{"b": 5, "__proto__": { "c": 6 }}';
> const a = JSON.parse(text);
> a;
{b: 5, __proto__: { c: 6 }}

a__proto__プロパティがあることに注意してください。これはプロトタイプリファレンスではありません。bと同様に、単純なオブジェクトプロパティキーです。最初の例からわかるように、プロトタイプの魔法を呼び出して実際のプロトタイプを設定するため、代入によってこのキーを作成することはできません。ただし、JSON.parse()は、その有害な名前で単純なプロパティを設定します。

それ自体では、JSON.parse()によって作成されたオブジェクトは完全に安全です。独自のプロトタイプはありません。組み込みのJavaScriptマジック名とたまたま重複する、一見無害なプロパティがあります。

ただし、他のメソッドはそれほど幸運ではありません

> const x = Object.assign({}, a);
> x;
{ b: 5}
> x.c;
6;

以前にJSON.parse()によって作成されたaオブジェクトを取得し、便利なObject.assign()メソッド(aのすべてのトップレベルプロパティのシャローコピーを提供された空の{}オブジェクトに実行するために使用されます)に渡すと、魔法の__proto__プロパティが「リーク」し、xの実際プロトタイプになります。

驚き!

外部テキスト入力を取得してJSON.parse()で解析し、そのオブジェクトを単純に操作(例:シャロークローンを作成してidを追加)し、検証ライブラリに渡すと、検出されずに__proto__を介して侵入します。

ああjoi!

もちろん、最初の質問は、なぜ検証モジュール**joi**はプロトタイプを無視して、潜在的に有害なデータを通過させるのかということです。私たちは自分自身に同じ質問をしました、そして私たちの即時の考えは「それは見落としだった」でした。バグ - 本当に大きな間違い。joiモジュールは、これが起こることを許可すべきではありませんでした。しかし…

joiは主にWeb入力データの検証に使用されますが、プロトタイプを持つ内部オブジェクトの検証にも使用しているユーザーベースがかなりいます。joiがプロトタイプを無視するという事実は、役立つ「機能」です。オブジェクト独自のvalidateプロパティを検証しながら、非常に複雑なプロトタイプ構造(多くのメソッドとリテラルプロパティを含む)を無視できます。

joiレベルのソリューションはすべて、現在機能しているコードを壊すことを意味します。

正しいこと

この時点で、私たちは壊滅的なセキュリティの脆弱性に直面していました。壮大なセキュリティ障害の上位層にあります。私たちが知っているのは、非常に人気のあるデータ検証ライブラリが有害なデータをブロックできないこと、そしてこのデータが簡単に侵入できることです。必要なのは、__proto__といくつかの不要なものをJSON入力に追加して、ツールを使用して構築されたアプリケーションに送信することだけです。

(劇的な一時停止)

これを防ぐためにjoiを修正する必要があることはわかっていましたが、この問題の規模を考えると、注目を集めすぎずに修正する方法で修正する必要がありました。少なくとも数日間は、ほとんどのシステムがアップデートを受信するまで、悪用を容易にしないようにします。

修正をこっそり行うことは、それほど難しいことではありません。コードの無意味なリファクタリングと組み合わせ、無関係なバグ修正といくつかのクールな新機能を追加すると、修正されている実際の問題に注意を引くことなく、新しいバージョンを公開できます。

問題は、正しい修正が有効なユースケースを壊してしまうことでした。joiは、設定したプロトタイプを無視するか、攻撃者によって設定されたプロトタイプをブロックするかを判断する方法がありません。エクスプロイトを修正するソリューションは、コードを壊し、コードを壊すと多くの注目を集める傾向があります。

一方、適切な(セマンティックバージョニングされた)修正をリリースし、それを重大な変更としてマークし、joiにプロトタイプで何をしたいかを明示的に伝える新しいAPIを追加すると、この脆弱性の悪用方法を世界と共有しながら、システムのアップグレードにかかる時間を増やすことになります(重大な変更はビルドツールによって自動的に適用されることはありません)。

迂回

当面の問題は着信リクエストペイロードに関するものですが、クエリ文字列、Cookie、ヘッダーを介して着信するデータにも影響を与える可能性があるかどうかを確認する必要がありました。基本的に、テキストからオブジェクトにシリアル化されるものはすべてです。

ノードのデフォルトのクエリ文字列パーサーとヘッダーパーサーに問題がないことをすぐに確認しました。base64でエンコードされたJSON Cookieと、カスタムクエリ文字列パーサーの使用に関する潜在的な問題を1つ特定しました。また、最も人気のあるサードパーティのクエリ文字列パーサーであるqsが脆弱でないことを確認するためのテストも作成しました(脆弱ではありません!)。

開発

このトリアージ全体を通して、汚染されたプロトタイプを含む問題の入力は、hapi.jsエコシステムを接続するWebフレームワークであるhapiからjoiに着信していると想定しました。Lobチームによるさらなる調査により、問題はもう少し微妙であることがわかりました。

hapiは、JSON.parse()を使用して着信データを処理しました。最初に結果オブジェクトを着信リクエストのpayloadプロパティとして設定し、処理のためにアプリケーションビジネスロジックに渡される前に、joiによる検証のために同じオブジェクトを渡しました。 JSON.parse()は実際には__proto__プロパティをリークしないため、無効なキーでjoiに到着し、検証に失敗します。

ただし、hapiは、検証前にペイロードデータを検査(および処理)できる2つの拡張ポイントを提供します。すべてが適切に文書化されており、ほとんどの開発者によく理解されています。拡張ポイントは、正当な(そして多くの場合セキュリティ関連の)理由で、検証前に生の入力と対話できるようにするためにあります。

これらの2つの拡張ポイントのいずれかで、開発者がObject.assign()または同様のメソッドをペイロードで使用した場合、__proto__プロパティがリークして実際プロトタイプになります。

安堵のため息

私たちは今、全く異なるレベルの酷さと向き合っていました。検証前にペイロードオブジェクトを操作することは一般的ではありません。つまり、これはもはや最悪のシナリオではなくなりました。依然として潜在的に壊滅的な状況でしたが、影響範囲はすべてのJoiユーザーから、非常に特殊な実装を行う一部のユーザーにまで縮小されました。

私たちはもはや、秘密裏のJoiリリースを検討する必要はありませんでした。Joiの問題はまだ残っていますが、今後数週間で新しいAPIと破壊的変更を含むリリースによって、適切に対処することができます。

また、フレームワークレベルではこの脆弱性を容易に軽減できることもわかっていました。フレームワークは、どのデータが外部から来たのか、どのデータが内部で生成されたのかを認識しているからです。開発者がこのような予期せぬミスを犯さないように保護できるのは、実際にはフレームワークだけです。

良いニュース、悪いニュース、何もない?

良いニュースは、これは私たちのせいではないということです。HapiやJoiのバグではありませんでした。HapiやJoiに特有ではない、複雑なアクションの組み合わせによってのみ可能でした。これは他のすべてのJavaScriptフレームワークでも発生する可能性があります。Hapiが壊れているなら、世界が壊れているということです。

素晴らしい - 責任のなすりつけ合いは解決しました。

悪いニュースは、(JavaScript自体以外に)責めるべきものが何もない場合、修正するのははるかに難しいということです。

セキュリティ問題が見つかったときに人々が最初に尋ねる質問は、CVEが公開されるかどうかです。CVE(Common Vulnerabilities and Exposures:共通脆弱性識別子)は、既知のセキュリティ問題のデータベースです。Webセキュリティの重要なコンポーネントです。CVEを公開することの利点は、すぐにアラームがトリガーされ、問題が解決されるまで自動ビルドに通知し、多くの場合中断させることです。

しかし、私たちはこれを何に結びつけるのでしょうか?

おそらく何もありません。Hapiのいくつかのバージョンに警告タグを付けるべきかどうか、まだ議論中です。「私たち」とは、ノードセキュリティプロセスです。デフォルトで問題を軽減するHapiの新しいバージョンができたので、修正と見なすことができます。しかし、修正はHapi自体にある問題に対するものではないため、古いバージョンが有害であると宣言するのは正確には適切ではありません。

人々に注意を喚起し、アップグレードを促すためだけに、以前のバージョンのHapiに関するアドバイザリを公開することは、アドバイザリプロセスの乱用です。セキュリティを向上させる目的でそれを乱用することには個人的には賛成ですが、それは私の判断ではありません。これを書いている時点では、まだ議論中です。

ソリューションビジネス

問題の軽減は難しくありませんでした。それをスケーラブルで安全にするには、もう少し手間がかかりました。有害なデータがシステムに侵入する場所と、問題のある`JSON.parse()`を使用している場所がわかっていたので、安全な実装に置き換えることができました。

1つの問題。データの検証にはコストがかかる可能性があり、私たちはすべての受信JSONテキストを検証することを計画しています。組み込みの`JSON.parse()`実装は高速です。本当に本当に高速です。より安全で、どこでも高速な代替品を構築することはできそうにありません。特に一晩で、新しいバグを発生させることなく行うことはできません。

既存の`JSON.parse()`メソッドをいくつかの追加ロジックでラップすることが明らかでした。オーバーヘッドをあまり追加しないようにする必要がありました。これはパフォーマンス上の考慮事項だけでなく、セキュリティ上の考慮事項でもあります。特定のデータを送信するだけでシステムの速度を簡単に低下させることができれば、非常に低いコストでDoS攻撃を実行することが容易になります。

私は途方もなくシンプルなソリューションを思いつきました。まず、既存のツールを使用してテキストを解析します。これが失敗しなかった場合は、元の生のテキストで問題のある文字列「__proto__」をスキャンします。見つかった場合にのみ、オブジェクトの実際のスキャンを実行します。「__proto__」へのすべての参照をブロックすることはできません。完全に有効な値である場合があります(ここでそれについて書いて、このテキストを公開するためにMediumに送信する場合など)。

これにより、「ハッピーパス」は以前とほぼ同じ速度になりました。関数呼び出しが1つ追加され、クイックテキストスキャン(これも非常に高速な組み込み実装)と条件付きリターンが追加されただけです。このソリューションは、それを通過すると予想されるデータの大部分にほとんど影響を与えませんでした。

次の問題。プロトタイププロパティは、受信オブジェクトのトップレベルにある必要はありません。深くネストすることができます。つまり、トップレベルに存在するかどうかを確認するだけでは不十分です。オブジェクトを再帰的に反復処理する必要があります。

再帰関数は好ましいツールですが、セキュリティを意識したコードを記述する場合には disastrous な場合があります。再帰関数は、ランタイムコールスタックのサイズを増やすからです。ループする回数が多いほど、コールスタックが長くなります。ある時点で - KABOOM - 最大長に達し、プロセスは停止します。

受信データの形状を保証できない場合、再帰的な反復は明白な脅威となります。攻撃者は、サーバーをクラッシュさせるのに十分な深さのオブジェクトを作成するだけで済みます。

私は、メモリ効率が高く(関数呼び出しが少なく、一時引数の受け渡しが少ない)、より安全なフラットループ実装を使用しました。自慢するためにこれを指摘しているのではなく、基本的なエンジニアリングプラクティスがどのようにセキュリティの落とし穴を作成(または回避)できるかを示すためです。

テストにかけよう

コードを2人に送信しました。最初にNathan LaFreniereにソリューションのセキュリティプロパティを再確認してもらい、次にMatteo Collinaにパフォーマンスを確認してもらいました。彼らはそれぞれの分野で最高の専門家であり、私がよく頼りにする人たちです。

パフォーマンステストベンチマークでは、「ハッピーパス」は事実上影響を受けないことが確認されました。興味深い発見は、問題のある値を削除する方が例外をスローするよりも高速だったことです。これは、私がbourneと呼ぶ新しいモジュールのデフォルトの動作をどうすべきかという疑問を提起しました。エラーにするか、サニタイズするかです。

ここでも懸念されたのは、アプリケーションをDoS攻撃にさらすことでした。`__proto__`を含むリクエストを送信すると処理速度が500%遅くなる場合、それは悪用されやすいベクトルとなる可能性があります。しかし、さらにテストを重ねた結果、**無効な**JSONテキストを送信すると、非常に似たようなコストが発生することが確認されました。

言い換えれば、JSONを解析する場合、無効な値は何が無効にするかに関係なく、より多くのコストがかかります。また、ベンチマークでは疑わしいオブジェクトのスキャンにかなりのコストがかかることが示されていますが、CPU時間の実際のコストは依然としてミリ秒単位であることを覚えておくことが重要です。注意して測定することは重要ですが、実際には有害ではありません。

Hapiのその後

感謝すべきことがたくさんあります。

Lobチームによる最初の開示は完璧でした。適切な人に、適切な情報で、非公開で報告されました。彼らは追加の調査結果をフォローアップし、私たちに適切な方法で解決するための時間と空間を与えてくれました。Lobはまた、長年にわたってHapiに関する私の仕事の主要なスポンサーでもあり、その財政的支援は他のすべてを可能にするために不可欠です。それについてはもう少し詳しく説明します。

Nicolas Morel、Nathan、Matteoのような人々が、喜んで助けてくれることは不可欠です。プレッシャーがなくても対処するのは容易ではありませんが、プレッシャーがあれば、適切なチームコラボレーションがなければミスが発生する可能性があります。

実際の脆弱性については幸運でした。壊滅的な問題のように見えたものが、繊細ですが、直接的な問題に対処できるようになりました。

また、ソースでそれを軽減するためのフルアクセス権を持っていたことも幸運でした。未知のフレームワークメンテナーにメールを送信して、迅速な回答を期待する必要はありませんでした。Hapiのすべての依存関係に対する完全な制御は、その有用性とセキュリティを再び証明しました。Hapiを使用していませんか?検討してみるべきかもしれません

めでたしめでたしの後

ここで、この事件を利用して、持続可能で安全なオープンソースのコストと必要性を改めて強調する必要があります。

この1つの問題に費やした私の時間だけで20時間を超えました。それは労働週の半分です。Hapiの新しいメジャーリリースを公開するためにすでに30時間以上費やした月の終わりに発生しました(作業のほとんどは12月に行われました)。これにより、私は今月5000ドル以上の個人的な経済的損失を被りました(時間を作るために有料のクライアントワークを削減する必要がありました)。

私が保守しているコードに依存している場合、これはまさにあなたが望む(そして正直に言って期待する)サポート、品質、コミットメントのレベルです。皆さんのほとんどは、私の仕事だけでなく、他の何百人もの献身的なオープンソースメンテナーの仕事を当然のことと思っています。

この仕事は重要なので、経済的に持続可能にするだけでなく、成長と拡大を図ることにしました。改善すべきことがたくさんあります。これはまさに、3月に導入される新しい商用ライセンスプランを実装する動機となっています。詳細についてはこちらをご覧ください。