はじめに
会社の社内勉強会で『安全なWebアプリケーションの作り方』1という本を選定し、シリアライゼーションに関する解説部分を担当することになって整理して見ました。本の内容を参考にしつつも、ほとんどの部分を自分なりに再構成してまとめました。
本記事では、プログラム間でデータを交換する際に利用されるシリアライゼーションの問題点と、シリアライゼーションで使用される媒介(データフォーマット)を扱う際に脆弱性を防ぐ方法について説明します。
本論
プログラミング言語の動作
- 作成したコードはコンピューターが実行可能な形に変換します(コンパイル/インタプリティングを行います)。
- 実行ファイルやバイトコードはメモリにロードされます。
- 変数や値はメモリに保存され、それぞれの値にはアドレスが割り当てられます。
- CPUは命令に従ってアドレスを参照しながら実行します。
人間が書くプログラムはメモリのアドレスを直接記述せず、コンピューターがコードを実行する際に自動的に各変数や値に対してメモリを割り当てます。
異なる2つのプログラム間のデータ交換
あるプログラムが他のプログラムのコードを実行するには、どの値がどのメモリアドレスにあるかを知る必要があります。しかし、これはコンピューター内部で行われる動作なので、他のプログラムの変数や値のアドレスを知ることはできません。他のプログラムの値を直接参照するコードを書くことは難しいです。OSはデフォルトでセキュリティのため、あるプログラムが他のプログラムのメモリにアクセスすることを遮断し、それぞれのプログラムが持つメモリ領域を分離しますので、他のプログラムのメモリにアクセスすることはできません。
異なるプログラム間でデータを交換するためには、特定のデータ交換のために用意されたプロトコル(ソケット、HTTP通信、gRPC、メッセージキュー、共有メモリ、ファイルなど)を使用する必要があります。
ウェブプログラミング
ウェブプログラミングは、サーバーとクライアントという別々のプログラム間でHTTP(S)通信というプロトコルを使ってデータを交換する構造を持っています。また、場合によってはサーバー間通信やサーバー内の他のプログラムとの通信も行われますので、これらのデータ交換のために様々なプロトコルが利用され、その際にシリアライゼーションが使用されます。ウェブアプリケーションとデータベース、ウェブアプリケーションとRedis、ウェブアプリケーションと他のウェブアプリケーション間の通信もすべてシリアライズの過程を経てデータが交換されます。
シリアライゼーション
異なるシステム間でデータをやり取りするための媒介体を作る過程をシリアライゼーション(serialization)といいますが、プログラミング言語においてのシリアライゼーションは、使用される値を他のプログラムやシステムに渡したり保存したりするために、同じプログラミング言語や他のプログラミング言語で扱える値として再構成可能な文字列やバイトデータに変換する過程です。
プログラミング言語はシリアライズされた文字列やバイトデータを受け取り、受け取ったプログラムの言語で使用できる値として利用できます。
デシリアライゼーション(deserialization)は、シリアライズされたデータを再びプログラミング言語で使用できる値に戻す過程です。
シリアライゼーション脆弱性
プログラミング言語はロジックを実行する役割を持っています。シリアライズされたデータに悪意のあるコードが含まれている場合、デシリアライズされた値を不注意に処理すると、その悪意のあるコードがプログラミング言語によって実行される可能性があります。
デシリアライゼーションの注意点
デシリアライズされたデータは、プログラミング言語で直接利用可能な形の値ですので、脆弱性を引き起こす可能性のある他のコードと組み合わせる際には注意が必要です。
eval
evalは文字列として記述されたコードを実行する機能です。デシリアライズされた文字列にevalを組み合わせると、その文字列がコードとして実行されてしまいます。このとき悪意のあるコードが含まれていれば、悪意のあるロジックが実行されます。
evalを安全に利用するには
- 必要がない限り、evalを使わない方法でロジックを構成します。
- デシリアライズされたデータを使用する前に、適切なフォーマットのデータであるかどうかを検証するバリデーションロジックを含めます。コードが含まれないように特殊文字を含めない形式のバリデーションなどを考慮します。
evalの代替方法
データが特定のフォーマットで構成できるのであれば、コードを直接実行するよりも、特殊なパターンの文字列やバイトを値に変換する機能を使用した方が良いです。
このような機能にはexplode、json_decode、unserializeなどがあります。これらは文字列データをプログラミング言語の値として利用できますが(unserializeを除く)、コードを実行しないためより安全です。
evalコードの例
🙅 悪い例:受け取ったコードの中に $arrInCode
という変数があると仮定します。evalでリクエストで受け取った値をそのまま実行しますので、任意コード実行の脆弱性があります。
use Illuminate\Http\Request;
Route::post('/extract-var', function (Request $request) {
$input = $request->input('code');
eval($input);
if (!isset($arrInCode)) {
return response()->json(['error' => 'No result from code execution.'], 400);
}
return response()->json(['result' => var_export($arrInCode, true)]);
});
🙆♂️ 良い例:dataパラメータのデータの受け渡しをjsonフォーマットで行いますので、任意コード実行の脆弱性がありません。
use Illuminate\Http\Request;
Route::post('/extract-var', function (Request $request) {
$input = $request->input('data');
if (!is_string($input)) {
return response()->json(['error' => 'Input must be a JSON string.'], 400);
}
$decoded = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return response()->json(['error' => 'Invalid JSON format.'], 400);
}
if (!is_array($decoded)) {
return response()->json(['error' => 'Decoded value is not an array.'], 400);
}
return response()->json(['result' => var_export($decoded, true)]);
});
unserializeの注意点
json_encode/json_decodeはjsonでパース可能な文字列への変換と復元機能を持ちます。phpのクラスをベースとしたオブジェクトをシリアライズしたい場合はserialize/unserializeを使います。オブジェクトのクラス情報と状態情報を表す文字列へ変換し、同じ状態のオブジェクトを復元するための機能です。
オブジェクトだけでなく他の値にも利用できますが、オブジェクトをシリアライズする際は、phpの他の機能よりもserialize/unserializeを使うのが最も簡単かつ確実な方法です。
unserialize
でオブジェクトを復元する際に、自動でコードが実行されるケースがあります。オブジェクトに __destruct
メソッドが存在する場合、このメソッドはオブジェクトが消滅する際(意図的削除、ガベージコレクタによる回収、phpプロセスが正常終了する時)に __destruct
メソッド内のコードを実行します。
unserialize
によってシリアライズされたデータのオブジェクトを復元し、生成されたオブジェクトが消滅する際に __destruct
メソッドのコードが実行されることで、デシリアライズしただけでevalのように文字列からコードが実行される問題が発生します。
シリアライズされたデータからオブジェクトを復元する際はクラス情報が必要です。phpコードが実行される時にデシリアライズ対象オブジェクトのクラスコードを含む先に読み込む必要があります。しかし、常にデシリアライズされるわけではありませんが、phpコードのクラス情報が漏洩したり、ファイルインジェクションを通じてアップロードされたphpファイルがオートローディングでクラスコードとしてphpエンジンに読み込まれると、オブジェクト生成され、 __destruct
メソッドが呼び出されることがあります。
unserialize脆弱性の対策
serialize/unserializeよりも可能な限りjson_encode/json_decodeを使用します。アプリケーション外部で定義可能なデータはunserialize
の対象から除外します。
オブジェクトをunserialize
関数でデシリアライズするにはアプリケーション側のクラス定義が必要なので、オブジェクトをシリアライズする必要がある場合はアプリケーション内部だけで共有し、外部サーバやクライアントに公開せず、サーバセッションやRedis、DBなどで利用します。
unserializeコードの例
🙅 悪い例:$data
にシリアライズされたオブジェクトが入っていれば、__destruct
メソッドによる脆弱性が実行される可能性があります。
use Illuminate\Http\Request;
Route::post('/bad-unserialize', function (Request $request) {
$data = $request->input('payload');
$result = @unserialize($data);
if ($result === false && $data !== serialize(false)) {
return response('unserialize failed: invalid data', 400);
}
return response()->json(['result' => 'unserialization succeed']);
});
🙆♂️ 良い例:SafeClassのコードは開発者が確認できますので安全です。テンプレート通りにシリアライズされたデータの状態を読み込んでオブジェクトを生成しますので、シリアライズデータにクラスに合わない部分があれば無視されてオブジェクト化されるため、脆弱性が発生しません。
use App\Models\SafeClass;
use Illuminate\Http\Request;
Route::post('/good-unserialize', function (Request $request) {
$data = $request->input('payload');
if (!is_string($data)) {
return response('payload must be a string', 400);
}
/**
* @link https://www.php.net/manual/en/function.unserialize.php
*/
$result = @unserialize($data, ['allowed_classes' => [SafeClass::class]]);
if ($result === false && $data !== serialize(false)) {
return response('unserialize failed: invalid data', 400);
}
return response()->json(['result' => 'unserialization succeeded']);
});
XML
マークアップ方式の文書ファイルを通じてデータを交換する方法として、代表的なのがXMLです。システム間のデータ交換手段としてXMLを使う場合、XMLの外部ファイルを読む機能が脆弱性を引き起こすことがあります。実行せずに読み込む機能だけでなぜ問題になるのか、次を見てみます。
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY variable SYSTEM "file:///etc/passwd">
]>
このようなXMLファイルがあるとします。このファイルをパースすると、Linuxユーザーの情報(ユーザー名、UID、ホームディレクトリ、デフォルトシェルなど)が書かれている/etc/passwdファイルを読み込んで表示する問題が発生します。これによって攻撃者はサーバの様々な情報にアクセスでき、情報が集まるとシステムがハッキングされる可能性が高まりますので、サーバ情報の漏洩を防ぐ対策が必要です。
単なる情報漏洩だけでなく、外部ファイルをパースする際にリソースが消費されることを利用し、ENTITYが外部ファイルを循環参照することでサーバに負荷をかけ障害を起こすこともできます。
また <!ENTITY xxe SYSTEM "http://internal-server/access_keys">
のように、内部ネットワークでのみアクセス可能なパスのデータを取得することもできます。
さらにXMLはシリアライズ/デシリアライズによるデータ交換の手段としても使われますので、プログラミング言語のオブジェクトにもデシリアライズ可能です。このため、unserialize
脆弱性のような__destruct
メソッドの呼び出しによるコード実行の問題も発生することがあります。
このようなXMLのENTITYタグを利用した攻撃手法を XXE(External XML Entity Injection)と呼びます。
XXE攻撃の対策
- 可能な限りXMLファイルをシリアライゼーションの中間手段ではなく、JSONを中間手段として利用する方法を検討します。
- 基本的にXMLファイルが外部データを参照できないよう、XMLパーサーで外部ENTITYの解釈を無効化する設定を行います。
- XMLファイルを扱うlibxml2ライブラリのバージョンを2.9以上にすると、XMLはデフォルトで外部ファイルを参照しません。
- phpの場合は
libxml_disable_entity_loader(true)
で無効化できます。基本的にphp8以降は外部ファイル参照がデフォルトで無効化されているため、この組み込み関数は非推奨扱いです。
最後に
何かを始めるとき、思慮深い人はきちんと見通しを立てますが、考えの足りない人は向こう見ずに手をつけて失敗します。
-
徳丸 浩、構造化データの読み込みにまつわる問題、『安全なWebアプリケーションの作り方 第2版』、SBクリエイティブ、343-370 ↩