NodeJSのC/C++アドオン拡張機能における脆弱性
Alessio Della Libera
2024年8月14日
0 分で読めますこの調査の主な目的の一つは、NodeJSのnpmパッケージにおけるC/C++の脆弱性を調べることでした。NodeJSのC/C++アドオンを対象に、バッファオーバーフロー、サービス拒否(プロセスのクラッシュ、型チェックの欠如)、メモリリークといった典型的な脆弱性を調査・特定し、関連するソース、シンク、サニタイザーをSnyk Codeでモデル化することに重点を置きます(Snyk brings developer-first AppSec approach to C/C++を参照)。
今回の調査対象は、実装の一部にC/C++インターフェースを使用しているNPMパッケージです。NPMに登録されていないプロジェクトは対象としていません。
このブログ記事では、NodeJSでC/C++アドオンを作成する際に発生しうる、一般的なセキュリティ脆弱性や脆弱なパターンの概要を紹介します。また、オープンソースのメンテナー向けに、修正例や提案も紹介します。
このブログ記事は、Cristian-Alexandru Staicu、Sazzadur Rahaman、Àgnes Kiss、Michael Backesによる論文「Bilingual Problems: Studying the Security Risks Incurred by Native Extensions in Scripting Languages」に着想を得ています。[1] 原論文では、JavaScriptを含む人気のプログラミング言語におけるネイティブ拡張機能のセキュリティリスクを分析しています。
NodeJSのC/C++アドオンの概要
NodeJSには、ネイティブのC/C++コードを呼び出すためのさまざまなAPIがあります。この調査では、次のいずれかの方法を使用した場合に発生しうるセキュリティ脆弱性を調べます。
node_api.h: Node-APInapi.h: Node-APIのC++ラッパー
上記のライブラリの使用例は、GitHubで確認できます。
アドオンとそのビルド方法について詳しくは、NodeJSの公式ドキュメントを参照してください。
少なくとも1つのパッケージで確認された脆弱性は次のとおりです。
メモリリーク
型チェックの欠如(DoS)
到達可能なassert(DoS)
未処理の例外(DoS)
バッファオーバーフロー
整数オーバーフロー
以下のセクションでは、脆弱なパターンの例と、脆弱性を悪用可能にするために満たす必要がある条件を説明します。
脆弱なパターンの例
このセクションでは、アドオン固有のAPIが適切に処理されない場合にセキュリティ上の問題を引き起こす仕組みと、この調査で特定した脆弱なパターンを見ていきます。
注: 以下の例は、網羅的なリストではありません。このブログ記事で取り上げていないシナリオも、セキュリティ上の問題につながる可能性があります。
このブログ記事では取り上げていないセキュリティ上の問題につながる可能性があります。
セットアップ
node-gypをインストールします(https://github.com/nodejs/node-gyp)。
次のセクションの例を実行するには、以下のファイルを使用します。
package.json
binding.gyp
C/C++拡張機能をビルドするには、次のコマンドを実行します。
node-gyp configurenode-gyp build
特定の例を実行する:
main.js
未処理の例外
影響: サービス拒否(DoS)
napi
napi APIには、例外を処理し、エラーをスローするためのさまざまな関数があります。ただし、binding.gypファイルで使用するフラグによっては、予期しないクラッシュを避けるために注意が必要です。
たとえば、binding.gypファイルでフラグNAPI_DISABLE_CPP_EXCEPTIONSが設定されている場合、次のシナリオでプロセスがクラッシュする可能性があります(DoS)。
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();など、エラーを生成するほかの関数(たとえば、引数の型が不正な場合)throw Napi::Error::Newがtry/catchで囲まれていない同じ関数内で到達可能な
returnのない複数のNapi::TypeError::New(env, "").ThrowAsJavaScriptException();
ドキュメントで説明されているように、「JavaScript例外をスローした後は、必要なクリーンアップを実行したうえで、通常はネイティブコールバックから直ちにreturnする必要があります。」
test_napi_exceptions.cpp
以下の例を実行します。
到達可能なassert
影響: サービス拒否(DoS)
node_api
提供されている例を見ると、一部の例では、関数の戻り値を確認するためにassertが使われていることがわかります。しかし、プログラムの実行中に、汚染された値(JavaScriptコード由来)によってassertに到達すると、クラッシュ(DoS)につながる可能性があります。いくつかのプロジェクトを確認したところ、コードロジック内に到達可能なassertが複数見つかったため、前述のリストに加える価値があると考えました。
このシナリオを修正するには、assertを使う代わりに、if内で戻り値を確認し、プログラムのロジックに応じて適切な値を返す方法が考えられます。
test_node_api_assert.c
この例を実行します。
型チェックされていないデータ
影響: サービス拒否(DoS)
napi
napiには、JavaScriptの型を変換するAPIが複数あります。たとえば、
Napi::Value::ToString()は「Napi::ValueをJavaScript文字列に変換して返します」。同様に、Napi::Value::ToNumber()は「Napi::ValueをJavaScriptの数値に変換して返します」。
内部では、napiのNapi::Value::ToString() APIはNode-APIのnapi_coerce_to_stringを呼び出します。
同様に、napiのNapi::Value::ToNumber() APIは内部でnapi_coerce_to_numberをNode-APIから呼び出します。
napi_coerce_to_stringに関する公式ドキュメントには、次のように記載されています。「このAPIは、ECMAScript言語仕様のセクション7.1.13で定義された抽象操作ToString()を実装します。渡された値がオブジェクトの場合、この関数はJavaScriptコードを実行する可能性があります。」つまり、ユーザー入力でtoStringプロパティが定義されている場合、toString()を呼び出すのではなく、そのプロパティの値が返され、予期しない結果につながる可能性があります。
Napi::Value::ToString()が返した値に対してほかのメソッドを呼び出すと、入力にtoStringプロパティが定義されている場合、例外が発生する可能性があり、多くの場合プロセスのクラッシュにつながります。同じことがnapi_coerce_to_numberにも当てはまります。
脆弱なパターン:
ToString()またはToNumberの結果であるNapi::Valueに対して、適切な型チェックをせずにNapi::String::Utf8Value()などを呼び出す
こうした問題を防ぐには、Napi::Value::ToString()またはNapi::Value::ToNumber()が返す値が、それぞれ文字列または数値であることを確認してから、ほかのメソッドを呼び出します。
注: 前述の未処理の例外と同様に、これらの問題はbinding.gypファイルでフラグNAPI_DISABLE_CPP_EXCEPTIONSが設定されている場合に発生します。
test_napi_unchecked_type.cpp
以下の例を実行します。
メモリリーク
影響: 情報漏えい
napi
napi APIには、UTF-8、UTF-16-LE、ISO-8859-1でエンコードされたC文字列からJavaScript文字列値を作成するメソッドが複数あります。これらのAPIは次のとおりです。
これらのメソッドはすべて同じシグネチャを持ちます。
特に注意して確認すべき値は[in] length、つまり文字列のバイト長です。この値が攻撃者によって制御される場合、またはハードコードされていて入力値が汚染されている場合、resultに予期しないメモリ値が格納される可能性があります。
この問題を避けるには、size_t lengthの値にNAPI_AUTO_LENGTHを使用します。
脆弱なパターン:
const char* strの長さを超えるsize_t lengthを指定してnapi_create_string_*を呼び出す
test_napi_memory_leak.c
この例を実行します。
方法
可能な限り多くの問題を自動的にテスト・発見するため、Snyk Codeの機能を活用して以下の方法を取りました。
NodeJSのアドオンAPIを使ってC/C++を呼び出すnpmパッケージのデータセットを作成する
Snyk Codeで、以下をモデル化するセキュリティルールを作成する:
定義したシンクとソースを使用するルールを作成し、汚染解析を実行してソースからシンクまで汚染を追跡する
既存のサポート対象ルール(たとえばバッファオーバーフローや整数オーバーフロー)で定義されているソースを利用し、NodeJSのアドオンAPIを使用するものだけでなく、さらに多くのC/C++脆弱性をカバーする
作成したルールを、先に構築したデータセットに対して実行する
結果を手動で確認し、必要に応じてPoCを作成する
この方法を使い、NodeJSアドオンに関連するAPIをモデル化してSnyk Codeで分析することで、npmパッケージ内の複数の問題を発見できました。
ただし、発見した問題の一部については、構築したデータセットからいくつかのプロジェクトを抽出し、手動で確認しました。
調査結果
この調査の結果、複数のパッケージで脆弱性が見つかりました。以下に示します。
まとめ
個人的には、いくつかの理由から、この調査は素晴らしい学びの機会となりました。NodeJSアドオンの世界を深く掘り下げ、既存の問題に関する文献を調べ、大規模なリポジトリ群の中から問題を発見するためにSnyk Codeを使ってシナリオをモデル化する機会を得られました。
JavaScriptやほかの多くのプログラミング言語にはかなり慣れていますが、C/C++は最近学び始めた言語です。これは、Snyk Codeのお客様が利用できる複数のセキュリティルールをサポートするために、私たちが取り組んできた(そして今も取り組んでいる)仕事がきっかけです。学びの機会と、Snyk Codeを使って複数のセキュリティ問題をモデル化する機会の両方を得られたこの調査を、とても楽しむことができました。この機会をくださったSnykに感謝します。
参考文献
Node-API - https://nodejs.org/api/n-api.html
node-addon-api - https://github.com/nodejs/node-addon-api
C++アドオン - https://nodejs.org/api/addons.html


