serialize()でシリアライズしたデータを外部に送信/保存1し、それをunserialize()すると危険です。
アンシリアライズは複雑なメモリ操作が必要で、PHPに限らず、何度もメモリ破壊攻撃の脆弱性が見つかっています。このため、外部入力データのアンシリアライズは行うべきではありません。現在のPHPプロジェクトでは、RubyやPythonと同じく、アンシリアライズのメモリ問題を”脆弱性として取り扱わない”ことになっています。
しかし、外部データでもunserialize()を安全に利用する方法はあります。
unserialize()が危険な理由
unserialize()には2つのリスクがあります。
- 故意に壊れたシリアル化データに改ざんされ、メモリ破壊攻撃が行われる
- 妥当なシリアル化データをそのまま再送され、再生攻撃が行われる
この2つを防止すれば安全に利用できます。
※ 上記以外に「シリアル化データの内容が漏洩する」があります。これには暗号で対策できますが、ここでは詳しく紹介しません。
serialize()/unserialize()を安全に利用する方法
serialize()/unserialize()を安全に利用するには暗号学的ハッシュ関数を利用します。最も便利な関数はhash_hkdf()です。
string hash_hkdf ( string
$algo
, string$ikm
[, int$length
= 0 [, string$info
= ” [, string$salt
= ” ]]] )
hash_hkdf()は汎用的な鍵導出関数です。serialize()/unserialize()を安全に利用する為にも利用できます。
URLにシリアル化データを埋め込む例:
<?php $secret_key = "random_bytes()で生成された強い鍵"; $salt = "random_bytes()で生成された強いsalt"; // 送信するデータ $numbers = [1234, 4567]; $expire = time() + 1800; // 30分後に有効期限切れ $raw_data = ['expire'=>$expire, 'data'=>$numbers]; $serialized_data = serialize($raw_data); // 送信するデータとその鍵の生成 $derived_key = bin2hex(hash_hkdf('SHA2-256', $secret_key, 0, $serialized_data, $salt)); // URLにシリアル化データを埋め込む場合 $url = 'https://example.com/?' . 'derived_key='. rawurlencode($derived_key) . '&salt=' . rawurlencode($salt) . '&serialized_data='. rawurlencode($serialized_data) ; ?>
シリアル化データをバリデーションしアンシリアライズする例:
<?php $secret_key = "random_bytes()で生成された強い鍵"; // 同じ鍵! // 送信されて来たデータを使い、送信時の鍵導出と同じ手順で鍵を導出 $sent_key = bin2hex(hash_hkdf('SHA2-256', $secret_key, 0, $_GET['serialized_data'], $_GET['salt'])); // 再導出した鍵と送られてきた導出鍵を比較 if (hash_equals($sent_key, $_GET['derived_key'] !== true) { die('攻撃を検出しました。処理を中止します。'); } // シリアル化データは改ざんされていないので安全にアンシリアライズ可能 $raw_data = unserialize($_GET['serialized_data']); // 有効期限をチェック if ($raw_data['expire'] < time()) { die('有効期限切れデータが送信されました。処理を中止します。'); } // $raw_data['data']は有効期限内で改ざんされていないことが保証されている
hash_hkdf()で利用するハッシュ関数が暗号学的に強いハッシュ関数であれば、シリアル化したデータの改ざんが無いこと、有効期限内であること、を検証できます。
HKDFハッシュは$infoパラメーターに何を設定しても構わないように設計されています。今回はそこにシリアル化したデータを設定し、安全性を保証しています。
この例ではhash_hkdf()で作りましたが、hash_hkdf()はHMACハッシュを使っています。hash_hmac()を使っても同じように検証可能です。詳しくはhash_hkdf()のページを参照してください。
※ $salt引数が”最後のオプション引数”になっていますが、$saltは秘密鍵防御に”最も重要な引数”です。HKDFを利用する場合、必ず指定しなければならないので注意してください。強い$saltを使用しないコードは脆弱なコードです。
暗号化との比較
シリアル化したデータを安全に利用する為に暗号も利用できます。(暗号化の方法はこちら)暗号化のメリットは言うまでもなく
- シリアル化したデータの内容を秘密にできる
ことにあります。
しかし、暗号化しただけでは”再生攻撃”(同じ暗号化データを送って、不正な処理を行わせる攻撃)が可能になります。再生攻撃を防ぎたい場合、暗号化したデータ内に有効期限を保存する必要があります。2
暗号を利用するとデータ全体を復号してから、有効期限をチェックする事になります。何度も有効期限切れが発生することが予想される場合、暗号だけを使った方法だとシステムへの負荷が多くなります。
データを秘密にする必要がない場合、ハッシュ関数を使う方法の方が優れている、と言えます。
※ ハードウェアAESが使える場合かなり効率良く復号できるので、もしかすると暗号の方が速いかも知れません。性能が気になる場合はチェックしてから使うと良いと思います。
まとめ
serialize()したデータを何も対策しないまま、外部に保存して再利用すると危険です。しかし、暗号学的ハッシュ関数を使用すれば、外部に保存したシリアル化データも安全に利用可能です。
参考: