Node.jsでのハッシュフラッディング脆弱性について…
今年の7月初め、Node.jsはハッシュフラッディング脆弱性に対処するため、現在保守されているすべてのブランチ向けにセキュリティアップデートをリリースしました。この暫定的な修正は、起動時のパフォーマンスに大きな回帰をもたらす代償があります。一方で、V8はパフォーマンスへの悪影響を避ける解決策を実装しました。
この投稿では、この脆弱性の背景と歴史、そして最終的な解決策について説明します。
ハッシュフラッディング攻撃
ハッシュテーブルはコンピュータサイエンスにおける最も重要なデータ構造の一つです。それはV8で広く使用されており、例えばオブジェクトのプロパティを格納するのに使われます。通常、新しいエントリの挿入は非常に効率的で、平均で𝒪(1)です。しかし、ハッシュ衝突が発生すると、最悪の場合𝒪(n)に達する可能性があります。つまり、n個のエントリを挿入するのに𝒪(n²)かかる場合があります。
Node.jsでは、HTTPヘッダがJavaScriptオブジェクトとして表現されています。ヘッダ名とその値のペアはオブジェクトプロパティとして格納されます。巧妙に準備されたHTTPリクエストを使うことで、攻撃者はサービス拒否攻撃を行うことが可能です。Node.jsプロセスは、最悪のハッシュテーブル挿入処理で忙しくなり、応答しなくなります。
この攻撃は2011年12月には公然とされたもので、多くのプログラミング言語に影響を与えることが示されました。それでは、V8やNode.jsがこの問題に本格的に取り組むまでにこれほど時間がかかったのはなぜでしょうか?
実際、公然とされた直後に、V8エンジニアはNode.jsコミュニティと共に緩和策を進めました。Node.js v0.11.8以降、この問題は対処されていました。この修正では、いわゆる_ハッシュシード値_が導入されました。ハッシュシードは起動時にランダムに選ばれ、特定のV8インスタンス内のすべてのハッシュ値をシードするために使用されます。攻撃者がハッシュシードを知らなければ、最悪の場合を引き起こすことが難しく、すべてのNode.jsインスタンスを標的とする攻撃を考案することはなおさら困難です。
この修正に関するコミットメッセージの一部をご紹介します:
このバージョンは、自分でV8をコンパイルするユーザーやスナップショットを使用しないユーザーに対してのみ問題を解決します。スナップショットベースで事前コンパイルされたV8では文字列ハッシュコードが予測可能なままです。
このバージョンは、自分でV8をコンパイルするユーザーやスナップショットを使用しないユーザーに対してのみ問題を解決します。スナップショットベースで事前コンパイルされたV8では文字列ハッシュコードが予測可能なままです。
スタートアップスナップショット
スタートアップスナップショットは、V8におけるエンジンの起動や新しいコンテキスト作成の速度を大幅に向上させるメカニズムです。初期オブジェクトや内部データ構造を一からセットアップする代わりに、V8は既存のスナップショットからデシリアライズを行います。スナップショット付きの最新のV8ビルドは、起動に3ms未満、新しいコンテキスト作成に1ミリ秒以下しかかかりません。一方でスナップショットがない場合、起動に200ms以上、新しいコンテキスト作成に10ms以上かかります。これは2桁の差を示しています。
あらゆるV8埋め込み実装がスタートアップスナップショットをどのように活用できるかについては以前の投稿で取り上げました。
事前ビルドされたスナップショットには、ハッシュテーブルや他のハッシュ値ベースのデータ構造が含まれています。スナップショットから初期化されると、これらのデータ構造を破損させずにハッシュシードを変更することはできません。スナップショットをバンドルしたNode.jsリリースには固定されたハッシュシードが含まれているため、緩和策は効果を失います。
これが、コミットメッセージで明示的に警告されていた内容です。
ほぼ解決、でもまだ完全ではない
2015年に進むと、新しいコンテキスト作成がパフォーマンスの面で回帰していることを報告するNode.jsの課題がありました。これは意外なことではなく、緩和策の一環としてスタートアップスナップショットが無効化されていたためです。しかし、その時点では、議論に参加していた全員が理由を知っているわけではありませんでした。
この投稿で説明したように、V8は擬似乱数生成器を使用してMath.randomの結果を生成します。各V8コンテキストにはランダム生成状態の独自のコピーがあります。これにより、Math.randomの結果がコンテキスト間で予測可能になることを防ぎます。
Random数生成器の状態は、コンテキストが作成された直後に外部ソースからシードされます。コンテキストがゼロから作成された場合でも、スナップショットからデシリアライズされた場合でも、関係ありません。
何らかの形で、Random数生成器の状態がハッシュシードと混同された。その結果、io.js v2.0.2以降、公式リリースの一部として事前構築されたスナップショットが導入されました。
二度目の試み
2017年5月、V8やGoogleのProject Zero、Google Cloud Platform内部での議論の中で、Node.jsがまだハッシュフラッディング攻撃に脆弱であることに気付きました。
Google Cloud PlatformのNode.js提供に関わるチームのAliとMylesの同僚から最初の対応がありました。彼らはNode.jsコミュニティと協力して、再び起動スナップショットをデフォルトで無効化する作業を行いました。今回はさらにテストケースを追加しました。
しかし、それだけでは終わらせたくありませんでした。起動スナップショットを無効化すると重要なパフォーマンスへの影響があります。これまでに、V8に多くの新しい言語 機能や高度な 最適化を追加しました。これらの追加により、ゼロからの起動がさらに高コストになっています。セキュリティリリース直後には、長期的な解決策の作業を開始しました。その目標は、起動スナップショットを再び有効化する際にハッシュフラッディングに対して脆弱にならないことでした。
提案された解決策の中から最も実用的なものを選択し、実装しました。スナップショットからデシリアライズした後、新しいハッシュシードを選択します。影響を受けるデータ構造は再ハッシュされ、一貫性を確保します。
実際のところ、通常の起動スナップショットでは影響を受けるデータ構造は少数です。そして幸いなことに、ハッシュテーブルの再ハッシュはその間にV8内で簡単に行えるようになっていました。この追加のオーバーヘッドは無視できる程度です。
起動スナップショットを再び有効化するためのパッチは、Node.jsにマージ され、最近のNode.js v8.3.0 リリースの一部となっています。