PHPのserialize()/unserialize()を安全に利用する方法

(Last Updated On: 2018年8月13日)

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()のページを参照してください。

出鱈目なシグニチャのhash_hkdf関数を安全に使う方法

※ $salt引数が”最後のオプション引数”になっていますが、$saltは秘密鍵防御に”最も重要な引数”です。HKDFを利用する場合、必ず指定しなければならないので注意してください。強い$saltを使用しないコードは脆弱なコードです。

暗号化との比較

シリアル化したデータを安全に利用する為に暗号も利用できます。(暗号化の方法はこちら)暗号化のメリットは言うまでもなく

  • シリアル化したデータの内容を秘密にできる

ことにあります。

しかし、暗号化しただけでは”再生攻撃”(同じ暗号化データを送って、不正な処理を行わせる攻撃)が可能になります。再生攻撃を防ぎたい場合、暗号化したデータ内に有効期限を保存する必要があります。2

暗号を利用するとデータ全体を復号してから、有効期限をチェックする事になります。何度も有効期限切れが発生することが予想される場合、暗号だけを使った方法だとシステムへの負荷が多くなります。

データを秘密にする必要がない場合、ハッシュ関数を使う方法の方が優れている、と言えます。

※ ハードウェアAESが使える場合かなり効率良く復号できるので、もしかすると暗号の方が速いかも知れません。性能が気になる場合はチェックしてから使うと良いと思います。

まとめ

serialize()したデータを何も対策しないまま、外部に保存して再利用すると危険です。しかし、暗号学的ハッシュ関数を使用すれば、外部に保存したシリアル化データも安全に利用可能です。

参考:

HMACハッシュの使い方のまとめ


  1. ブラウザのクッキー、URLやフォームのパラメーターとして送信するなど。 
  2.  hash_hkdf()などのハッシュ関数を使って有効期限をチェックする事も可能です。鍵導出を使い、複合前に改ざんと有効期限をチェックすることも可能です。 

投稿者: yohgaki