【PHP8.x】OverflowException::__wakeup()メソッドの使い方
__wakeupメソッドの使い方について、初心者にもわかりやすく解説します。
基本的な使い方
『__wakeupメソッドは、シリアライズされたOverflowExceptionオブジェクトがunserialize()関数によって復元される際に、自動的に呼び出されるマジックメソッドです。オブジェクトのシリアライズとは、オブジェクトの状態を保存や転送が可能な形式(一般的には文字列)に変換する処理のことです。unserialize()はその逆で、文字列から元のオブジェクトを復元します。__wakeupメソッドの本来の役割は、この復元の過程で、失われたデータベース接続の再確立や必要なリソースの再初期化などを行い、オブジェクトが再び正常に機能できる状態を準備することです。しかし、OverflowExceptionはPHPの基本的な例外クラスであるExceptionを継承しています。セキュリティ上の脆弱性につながる可能性があるため、Exceptionクラスとその派生クラスのオブジェクトをシリアライズおよびデシリアライズすることは強く非推奨とされています。このため、OverflowExceptionオブジェクトに対してunserialize()を実行する状況は避けるべきです。実際にPHP 8以降では、このメソッドが呼び出されると警告が発生します。したがって、このメソッドは開発者が直接呼び出して使用するものではなく、PHPの内部的な仕組みと、その非推奨の挙動を理解するために知っておくべきメソッドです。
構文(syntax)
1final public function __wakeup(): void
引数(parameters)
引数なし
引数はありません
戻り値(return)
戻り値なし
戻り値はありません
サンプルコード
PHP __wakeup bypass 脆弱性デモ
1<?php 2 3/** 4 * このコードは、古いPHPバージョンに存在した __wakeup() マジックメソッドの 5 * バイパス脆弱性 (CVE-2016-7124) の概念を説明するためのサンプルです。 6 * 7 * シリアル化された文字列内のプロパティ数を実際の数より多く偽ることで、 8 * unserialize() 実行時に __wakeup() の呼び出しをスキップさせる攻撃手法でした。 9 * 10 * 注意: この脆弱性はPHP 7.0.10以降で修正済みです。 11 * 現代のPHP 8でこのコードを実行すると、プロパティ数の不整合により 12 * unserialize() は false を返し、NOTICE が発生するため、バイパスは成功しません。 13 */ 14class VulnerableObject 15{ 16 public string $message = 'This is a safe default message.'; 17 18 /** 19 * unserialize() 時に自動的に呼び出されるマジックメソッド。 20 * 本来はオブジェクトの状態を検証・復元するために使用されます。 21 * この脆弱性はこのメソッドの呼び出しを回避します。 22 */ 23 public function __wakeup(): void 24 { 25 echo "!!! __wakeup() was called. The object state is being validated." . PHP_EOL; 26 // 安全のため、デシリアライズ時にプロパティをリセットする例 27 $this->message = 'This is a safe default message.'; 28 } 29} 30 31// 1. オブジェクトをインスタンス化し、シリアル化する 32$object = new VulnerableObject(); 33$object->message = 'This is a malicious payload.'; 34$serializedString = serialize($object); 35 36echo "Original serialized string:" . PHP_EOL; 37echo $serializedString . PHP_EOL; 38// 出力例: O:16:"VulnerableObject":1:{s:7:"message";s:27:"This is a malicious payload.";} 39 40// 2. プロパティ数を不正に書き換える(1 -> 2) 41// これにより、脆弱なPHPバージョンは __wakeup() の呼び出しをスキップしていました。 42$maliciousString = str_replace('O:16:"VulnerableObject":1:', 'O:16:"VulnerableObject":2:', $serializedString); 43 44echo PHP_EOL . "Maliciously modified string (property count changed from 1 to 2):" . PHP_EOL; 45echo $maliciousString . PHP_EOL; 46 47// 3. 改ざんした文字列をデシリアライズする 48echo PHP_EOL . "Attempting to unserialize the malicious string..." . PHP_EOL; 49$result = unserialize($maliciousString); 50 51// 4. 結果の確認 52echo PHP_EOL . "Result of unserialization:" . PHP_EOL; 53var_dump($result); 54 55if ($result instanceof VulnerableObject) { 56 // 脆弱なPHPバージョンでは、__wakeup()が呼ばれずにオブジェクトが復元される 57 echo "__wakeup() bypass SUCCEEDED (behavior on old PHP versions)." . PHP_EOL; 58 echo "Message property: " . $result->message . PHP_EOL; 59} else { 60 // 現代のPHPバージョンでは、デシリアライズが失敗する 61 echo "__wakeup() bypass FAILED (behavior on modern PHP versions)." . PHP_EOL; 62}
__wakeupは、unserialize()関数によってシリアル化された文字列からオブジェクトが復元される際に、自動的に呼び出されるマジックメソッドです。このメソッドは引数を持たず、戻り値もありません。主な用途は、オブジェクトが復元された後の状態を検証したり、必要な初期化処理を行ったりすることです。
このサンプルコードは、過去のPHPバージョンに存在した__wakeupメソッドの呼び出しを意図的に回避できてしまう脆弱性の概念を説明しています。攻撃者は、シリアル化された文字列に含まれるオブジェクトのプロパティ数を、実際の数よりも大きな値に改ざんします。
コードの流れとして、まずオブジェクトをserialize()関数で文字列に変換し、その文字列内のプロパティ数を手動で書き換えています。脆弱性があった古いPHPでは、この不正な文字列をunserialize()関数で処理すると、プロパティ数の不整合が原因で__wakeupメソッドの実行がスキップされてしまい、本来行われるべき検証処理を回避できました。
しかし、この脆弱性はすでに修正済みです。PHP 8のような現代のバージョンでこのコードを実行すると、unserialize()関数はプロパティ数の不整合を検知して処理に失敗し、falseを返します。これにより、__wakeupメソッドがバイパスされることはなく、安全性が確保されています。
このサンプルコードが示す__wakeupメソッドのバイパス脆弱性は、古いPHPバージョンに存在したもので、PHP 7.0.10以降では修正済みです。そのため、PHP 8でこのコードを実行すると、コメントの通りデシリアライズは失敗しfalseが返りますが、これは意図された正常な動作です。最も重要な注意点は、この脆弱性の有無とは別に、ユーザー入力など信頼できない外部からの文字列を安易にunserialize関数でオブジェクトに戻す処理は、意図しないコードが実行される危険性があるため、原則として避けるべきだという点です。データのやり取りには、より安全なJSON形式などを使用することが推奨されます。
PHP __wakeup によるデシリアライズ失敗
1<?php 2 3/** 4 * OverflowExceptionのインスタンスをシリアライズ・デシリアライズし、 5 * __wakeupマジックメソッドの挙動を確認するサンプルクラスです。 6 */ 7class WakeupExample 8{ 9 /** 10 * メインの処理を実行します。 11 * 12 * Exceptionクラスとそのサブクラスは、セキュリティ上の理由からデシリアライズが許可されていません。 13 * unserialize()が実行されると、内部的に__wakeup()メソッドが呼び出されますが、 14 * Exceptionクラスに定義されている__wakeup()は、意図的に処理を失敗させます。 15 * このコードは、その挙動を実証します。 16 */ 17 public static function run(): void 18 { 19 // 1. OverflowExceptionのインスタンスを作成します。 20 $exception = new OverflowException("コンテナの容量を超えました。"); 21 echo "OverflowExceptionのインスタンスが作成されました。" . PHP_EOL; 22 23 // 2. serialize()関数でオブジェクトを文字列に変換します。 24 $serializedData = serialize($exception); 25 echo "オブジェクトがシリアライズされました: " . $serializedData . PHP_EOL; 26 27 // 3. unserialize()関数で文字列からオブジェクトを復元しようと試みます。 28 // この時、内部で__wakeup()が呼び出されますが、例外を発生させて失敗します。 29 try { 30 echo "デシリアライズを試みます..." . PHP_EOL; 31 // unserializeの第二引数に false を指定すると、未定義クラスの読み込みを試みなくなります。 32 // ここでは false を指定して、クラスのオートロードに関する警告を抑制しています。 33 unserialize($serializedData, ['allowed_classes' => false]); 34 } catch (Throwable $e) { 35 // 4. __wakeup()の働きにより、デシリアライズが失敗し、例外がスローされます。 36 echo "デシリアライズに失敗しました。" . PHP_EOL; 37 echo "エラーメッセージ: " . $e->getMessage() . PHP_EOL; 38 } 39 } 40} 41 42// サンプルコードの実行 43WakeupExample::run();
OverflowExceptionクラスの__wakeupメソッドは、unserialize()関数によってオブジェクトが復元(デシリアライズ)される際に、自動的に呼び出される特別なメソッドです。これは「マジックメソッド」の一種で、引数はなく、戻り値も返しません。
サンプルコードは、まずOverflowExceptionのインスタンスを生成し、serialize()関数で文字列に変換しています。次に、その文字列をunserialize()関数で元のオブジェクトに戻そうと試みます。
しかし、PHPではセキュリティ上の理由から、Exceptionクラスおよびその派生クラス(OverflowExceptionなど)のデシリアライズは意図的に禁止されています。unserialize()が実行されると、内部で__wakeupメソッドが呼び出されますが、このメソッドはデシリアライズ処理を強制的に失敗させるために例外を発生させるように設計されています。その結果、サンプルコードのunserialize()は失敗し、catchブロックでエラーが捕捉されます。このように、OverflowExceptionの__wakeupメソッドは、安全でない操作を防ぐためのセキュリティ機構として機能しています。
__wakeupは、unserialize()関数でオブジェクトが復元される際に自動で呼び出される特殊なメソッドです。PHPではセキュリティ上の理由から、Exceptionクラスとそのサブクラス(サンプルコードのOverflowExceptionなど)のデシリアライズは許可されていません。これらのクラスに実装されている__wakeupメソッドは、意図的に処理を失敗させ、例外を発生させます。そのため、サンプルコードでエラーが発生するのはバグではなく、PHPの仕様に沿った正しい挙動です。これは、例外オブジェクトの内部状態が不正に操作されるのを防ぐための仕組みです。一般的に、信頼できないデータをunserialize()で扱う際は、脆弱性の原因になりうるため注意が必要です。