Bufferの脆弱性を悪用する
2016年4月5日
0 分で読めますNode.jsの便利な機能の陰には、Bufferという時限爆弾が潜んでいます。この危険なクラスを誤って扱うと、サーバー側のメモリが簡単に漏えいし、その中にある秘密情報やキーまで露出するおそれがあります。これはFeross AboukhadijehとMathias Buss Madsenが公表した脆弱性によるものです。人気のある優れたプロジェクトもこの問題に直面しており、npmパッケージのmongoose、ws、request、そして最近ではsequelizeにも、広範囲に影響する脆弱性が持ち込まれました。
この記事では、Bufferの仕組みと、なぜそのように動作するのかを解説します。脆弱なアプリケーションに対してエクスプロイトを実行し、その影響を具体的に示します。最後に、Node 6で予定されている変更と、Bufferのデフォルト動作から身を守るためにできる5つの対策を紹介します。
どのようにしてこの問題が起きたのでしょうか?
クライアントサイドのJavaScriptでは、メモリ割り当てを意識する必要がありません。多くの言語と同様に、JSでは基盤となるエンジン(V8など)が必要に応じてメモリを割り当て、ガベージコレクションを行うため、コーディングがより簡単で安全になります。またブラウザーでは、JSが動作するサンドボックスを維持するために、メモリへのアクセスを防ぐことも必要です。
Node.jsによってJSがサーバーサイドに広がると、ブラウザーのサンドボックスはなくなり、バイナリデータを簡単かつ高速に処理する必要性が高まりました。こうしたニーズに応えるため、Node.jsはバイナリデータを扱うBufferクラスを導入しました。なお、後にES6では、TypedArrayやArrayBufferなど、バイナリデータ向けのクラスが追加されました。
Bufferは変更可能なバイナリデータの配列で、文字列、配列、数値を使って初期化できます。node.jsのドキュメントから一部を抜粋します。
最初の2つの形式は、受け取った値を単純にバイナリ表現に変換します。一方、最後の形式は指定されたサイズのバッファーを事前に確保します。そのため、特にストリームからデータを読み込む際に、まさにバッファーとして便利に使えます。
セキュリティ上の欠陥
Bufferのnumberコンストラクターを使うと、メモリは割り当てられますが、ゼロで埋められることはありません。代わりに、割り当てられたバッファーには、その時点でメモリに入っていたデータがそのまま残ります。ターミナルでNode.jsを実行し、指定したサイズのBufferを繰り返し作成してみると、この動作を確認できます。以前使われていたメモリの別の領域を指すため、作成するたびに異なる値が含まれます。
これはよく知られた動作で、buf.fill(0)を呼び出すだけで割り当てられたメモリをゼロで埋められます。しかし、ゼロ初期化を忘れると、メモリを漏えいさせる可能性があります。メモリの露出だけなら大した問題ではないように思えるかもしれませんが、露出する可能性のある秘密情報(キー、ソースコード、システム情報など)の量を考えてみてください。2014年に発生したOpenSSLの大規模な脆弱性Heartbleedも、メモリ露出の脆弱性でした。
セキュリティリスクを実証する
Bufferの脆弱性をより深く理解するため、Goofという脆弱なアプリケーションを見てみましょう。コードはGitHubで公開されており、自分でエクスプロイトを実行したい場合に備えて、インストール手順も記載されています。

これはシンプルな(そして脆弱な)TODOリストアプリです。保存された項目をMongoDBとやり取りするためにmongooseを使っており、スキーマは次のようになっています。
ご覧のとおり、実際のTODOタスクを格納するcontentフィールドの型はBufferで、バイナリデータに対応しています。多くのNode.jsアプリと同様に、snyk-demo-todoもJSON APIを公開しており、これを使ってコマンドラインから項目を作成します。
content変数の値はBufferコンストラクターに渡され、短い文字列として初期化された後、データベースに書き込まれました。新しい項目はアプリケーションを開くと表示され、レスポンスにも(Base64エンコードされた状態で)返されます。

では、脆弱性を悪用してみましょう。同じリクエストを送信しますが、"Buy Milk"という文字列を引用符で囲まない数値800に置き換えます。
先ほどと同様に、contentの値はBufferコンストラクターに渡されます。しかし今度は値の型がnumberであるため、もう一方のBufferコンストラクターが呼び出されます。これにより、初期化されていない800バイトのメモリでTODO項目が初期化され、データベースに保存されます。このデータはHTMLレスポンスにも表示されます(バイナリデータなので読みにくいものの)。さらにBase64エンコードされた後、curlにも返されます。

この呼び出しを何度も繰り返すことで、より多くのメモリ領域を取得し、Base64デコードできます。疑われないよう、作成したメモをその都度削除しながら、データを確認したり、secret、ssh-key、ソースコードなどのパターンを検索したりすることになるでしょう。
どうすれば修正できるでしょうか?
サンプルアプリケーションの脆弱性は、依存関係のmongooseにありました。修正では、数値を配列に変換し、数値を長さではなく単一の値として扱うようにしています。脆弱なバージョンのmongooseを使用している場合は、新しいバージョンにアップグレードしてください。使用しているかどうかわからない場合は、Snykを使って、こうした脆弱性やその他の脆弱性を検出・修正できます。
脆弱性が自分のコードにある場合は、numberコンストラクターの使用を禁止する(必要に応じて配列や文字列に変換する)か、呼び出すたびにfill(0)関数を使う方法があります。メモリをゼロで埋める処理は効率的ですが、大きなバッファーでは時間がかかるため、アプリケーションへのパフォーマンスの影響を考慮してください。
Node 6で予定されている変更
Bufferのこのデフォルト動作は懸念事項であり、複数の既知の脆弱性の原因にもなっています。しかし、Nodeの現行バージョンで変更するとアプリケーションが動かなくなる可能性があるため、変更は容易ではありません。とはいえ、Node 6以降では、メモリをゼロ初期化するallocと、ゼロ初期化しないallocUnsafeを明示的に使うことが推奨されています。この明示的なAPIにより、開発者が同様のミスを避けやすくなります。
デフォルトのコンストラクターも引き続きサポートされ、同じように動作しますが、非推奨となり、使用が推奨されなくなります。また、Node 6以降では--zero-fill-buffersフラグを指定することで、デフォルトのBuffer数値コンストラクターが割り当てたバッファーをゼロで埋めるようにできます。
安全を守るための5つの対策
Bufferは便利ですが、危険を伴うクラスです。使うことを検討している場合は、次の点を確認してください。
代わりに
TypedArrayやArrayBufferを使えませんか?これらのクラスもバイナリデータに対応しており、メモリをゼロで初期化します。numberコンストラクターの使用を禁止できますか?可能であれば、mongooseと同様に禁止しましょう。buf.fill(0)を使って、割り当てたデータをゼロで埋められますか?多少のパフォーマンスへの影響はありますが、より安全になります。上記の対策をどうしてもすべて実施できない場合は、
Bufferの内容がどこへ渡るのかを慎重に追跡してください。細心の注意を払ってください。依存関係にnpmパッケージを使用している場合は、snyk wizardを実行して、依存関係にある
Buffer関連の脆弱性を修正しましょう。
キャプチャー・ザ・フラッグを始めよう
オンデマンドのバーチャル入門ワークショップを視聴して、キャプチャー・ザ・フラッグの課題の解き方を学びましょう。


