【PHP8.x】UnderflowException::__wakeup()メソッドの使い方
__wakeupメソッドの使い方について、初心者にもわかりやすく解説します。
基本的な使い方
『__wakeupメソッドは、unserialize()関数によってオブジェクトが復元される際に、必要な初期化処理を実行するメソッドです。このメソッドはPHPのマジックメソッドの一つであり、オブジェクトがシリアライズされた状態から復元される直前に自動的に呼び出されます。開発者はこのメソッドを利用して、データベース接続の再確立や、一時的に失われていたリソースの再初期化など、オブジェクトが再び利用可能になるための準備処理を記述できます。しかし、UnderflowExceptionクラスにおける__wakeupメソッドの動作には注意が必要です。UnderflowExceptionはPHPの標準例外クラスであるExceptionを継承しています。セキュリティ上の理由から、PHP 8.0以降ではExceptionおよびそのサブクラスのデシリアライズは意図的に制限されています。そのため、シリアライズされたUnderflowExceptionオブジェクトをunserialize()しようとすると、この__wakeupメソッドが呼び出された際に例外がスローされ、処理が失敗します。これは、悪意のあるシリアライズデータによって予期せぬオブジェクトが生成されることを防ぐための措置です。したがって、このメソッドはUnderflowExceptionクラスでは実質的にデシリアライズを禁止する役割を担っています。』
構文(syntax)
1final public __wakeup(): void
引数(parameters)
引数なし
引数はありません
戻り値(return)
戻り値なし
戻り値はありません
サンプルコード
PHP __wakeup バイパス脆弱性試行
1<?php 2 3/** 4 * __wakeup() マジックメソッドの挙動を確認するためのクラス。 5 * このメソッドは unserialize() 時に自動的に呼び出されます。 6 */ 7class UserObject 8{ 9 public string $name = 'guest'; 10 11 /** 12 * オブジェクトがデシリアライズされる際に呼び出され、プロパティを再初期化します。 13 */ 14 public function __wakeup(): void 15 { 16 // 脆弱性が存在しない場合、このメッセージが表示され、$name は 'default' に上書きされます。 17 echo "__wakeup() が呼び出されました。\n"; 18 $this->name = 'default'; 19 } 20} 21 22// PHP 7.3 以前に存在した __wakeup() のバイパス脆弱性を試行するペイロードです。 23// この脆弱性は、シリアライズされた文字列内のプロパティ数を、実際の数より 24// 大きく改ざんすることで __wakeup() の呼び出しをスキップするものでした。 25// 26// 元のオブジェクトをシリアライズした場合の文字列: 27// O:10:"UserObject":1:{s:4:"name";s:5:"guest";} 28// ^ 29// プロパティ数(1) 30// 31// 下記のペイロードでは、プロパティ数を意図的に 1 から 2 に書き換えています。 32$payload = 'O:10:"UserObject":2:{s:4:"name";s:5:"guest";}'; 33 34echo "改ざんされた文字列でデシリアライズを試みます...\n"; 35echo "ペイロード: " . $payload . "\n\n"; 36 37// PHP 7.4 以降では、この脆弱性は修正されています。 38// 不正なプロパティ数を持つ文字列を unserialize() しようとすると、 39// 警告 (Notice) が発生し、処理は失敗して false を返します。 40// そのため、__wakeup() がバイパスされてオブジェクトが不正に復元されることはありません。 41$object = unserialize($payload); 42 43if ($object === false) { 44 echo "結果: デシリアライズに失敗しました。\n"; 45 echo "PHP 7.4以降のバージョンでは、この脆弱性は修正されており安全です。\n"; 46} else { 47 // このブロックが実行されるのは脆弱性が存在する古いPHPバージョンの場合のみです。 48 // その場合、__wakeup() は呼び出されず、name プロパティは 'guest' のまま残ります。 49 echo "結果: デシリアライズに成功しました。(古いPHPでの挙動)\n"; 50 echo "ユーザー名: " . $object->name . "\n"; 51}
__wakeup()は、unserialize()関数によってシリアライズ化された文字列からオブジェクトが復元される際に、自動的に呼び出される「マジックメソッド」です。オブジェクトが「目覚める」タイミングで、データベース接続の再確立やプロパティの再初期化といった、復元後の準備処理を記述するために使用されます。このメソッドは引数を受け取らず、何も値を返しません。
サンプルコードは、過去のPHPに存在した__wakeup()メソッドの呼び出しを意図的に回避する「バイパス脆弱性」を実演しています。この脆弱性は、シリアライズされた文字列データに含まれるプロパティの数を不正に書き換えることで、__wakeup()メソッドを実行させずにオブジェクトを復元させるという攻撃手法でした。
しかし、この脆弱性はPHP 7.4以降のバージョンで修正されています。現在の安全なPHP環境でこのコードを実行すると、不正なデータによるデシリアライズは失敗し、falseが返されます。これにより、__wakeup()メソッドに記述された初期化処理がスキップされてしまう危険性はなく、安全性が確保されています。
__wakeupは、unserialize関数でオブジェクトが復元される際に自動で呼び出されるマジックメソッドで、主に初期化処理に使われます。このサンプルコードは、過去のPHPバージョンに存在した、シリアライズされた文字列を改ざんして__wakeupの実行を回避する脆弱性を再現したものです。PHP 7.4以降ではこの脆弱性は修正されており、不正なデータでunserializeを試みると処理が失敗するため安全です。しかし、どのようなバージョンであっても、外部から受け取った信頼できない文字列をunserializeすることは、他の未知の脆弱性を突かれる危険性があるため避けるべきです。データのやり取りには、より安全なJSON形式の利用を推奨します。
PHP __wakeup を使ったリソース再初期化
1<?php 2 3/** 4 * __wakeupマジックメソッドの使用例を示すクラス 5 * 6 * オブジェクトがデシリアライズ(unserialize)される際に、 7 * ファイル接続のような失われたリソースを再初期化する方法を示します。 8 */ 9class FileLogger 10{ 11 private string $filePath; 12 private $fileHandle; // ファイルリソースは型宣言できません 13 14 /** 15 * コンストラクタ: ファイルパスを受け取り、ファイルを開きます。 16 * 17 * @param string $filePath ログファイルのパス 18 */ 19 public function __construct(string $filePath) 20 { 21 $this->filePath = $filePath; 22 $this->openFile(); 23 echo "インスタンスが生成され、ファイルが開かれました。\n"; 24 } 25 26 /** 27 * ログをファイルに書き込みます。 28 * 29 * @param string $message ログメッセージ 30 */ 31 public function log(string $message): void 32 { 33 if ($this->fileHandle) { 34 fwrite($this->fileHandle, date('Y-m-d H:i:s') . ' - ' . $message . PHP_EOL); 35 echo "「{$message}」をログに書き込みました。\n"; 36 } 37 } 38 39 /** 40 * デストラクタ: オブジェクトが破棄される際にファイルを閉じます。 41 */ 42 public function __destruct() 43 { 44 if (is_resource($this->fileHandle)) { 45 fclose($this->fileHandle); 46 } 47 echo "インスタンスが破棄され、ファイルが閉じられました。\n"; 48 } 49 50 /** 51 * __sleep: シリアライズ時に保存するプロパティを指定します。 52 * ファイルハンドル($fileHandle)のようなリソースはシリアライズできないため、 53 * ここには含めません。 54 * 55 * @return string[] シリアライズするプロパティ名の配列 56 */ 57 public function __sleep(): array 58 { 59 echo "__sleep() が呼び出されました。ファイルハンドルを破棄します。\n"; 60 return ['filePath']; 61 } 62 63 /** 64 * __wakeup: デシリアライズ(unserialize)後に自動的に呼び出されます。 65 * シリアライズ時に失われたファイルハンドルを再初期化します。 66 */ 67 public function __wakeup(): void 68 { 69 echo "__wakeup() が呼び出されました。ファイルを再オープンします。\n"; 70 $this->openFile(); 71 } 72 73 /** 74 * ファイルを追記モードで開きます。 75 */ 76 private function openFile(): void 77 { 78 // 'a' は追記モードを意味します。ファイルがなければ作成されます。 79 $this->fileHandle = fopen($this->filePath, 'a'); 80 } 81} 82 83// --- 実行コード --- 84 85$logFilePath = 'app.log'; 86 87// 1. オブジェクトを生成し、ログを書き込む 88echo "--- 最初のオブジェクト操作 ---\n"; 89$logger = new FileLogger($logFilePath); 90$logger->log('プログラム開始'); 91 92// 2. オブジェクトをシリアライズ(文字列に変換)する 93// このとき __sleep() が呼ばれる 94echo "\n--- シリアライズ処理 ---\n"; 95$serializedLogger = serialize($logger); 96echo "シリアライズされたデータ: " . $serializedLogger . "\n"; 97 98// 3. 元のオブジェクトを破棄する 99// このとき __destruct() が呼ばれる 100unset($logger); 101 102// 4. シリアライズされた文字列からオブジェクトを復元する 103// このとき __wakeup() が呼ばれ、ファイルハンドルが再生成される 104echo "\n--- デシリアライズ処理 ---\n"; 105$restoredLogger = unserialize($serializedLogger); 106 107// 5. 復元されたオブジェクトでログを書き込む 108echo "\n--- 復元されたオブジェクト操作 ---\n"; 109$restoredLogger->log('プログラム再開'); 110 111echo "\n"; 112 113// ログファイルの内容を確認 114if (file_exists($logFilePath)) { 115 echo "--- ログファイルの内容 ---\n"; 116 echo file_get_contents($logFilePath); 117 unlink($logFilePath); // 後片付け 118}
__wakeupは、PHPのマジックメソッドの一つです。このメソッドは、unserialize()関数によってシリアライズされた文字列からオブジェクトが復元(デシリアライズ)される際に、自動的に呼び出されます。
__wakeupの主な目的は、オブジェクトの復元時に必要な初期化処理を実行することです。特に、ファイルハンドルやデータベース接続といった「リソース」は、serialize()で文字列として保存することができません。そのため、オブジェクトがデシリアライズされた際には、これらの失われたリソースを再確立する必要があります。__wakeupは、まさにこの再接続や再初期化の処理を定義するために使用されます。
サンプルコードのFileLoggerクラスでは、serialize()時に保存されなかったファイルハンドルを、unserialize()のタイミングで呼び出される__wakeup()メソッド内で再度オープンしています。これにより、復元されたオブジェクトでも再びファイルへの書き込みが可能になります。このメソッドは引数を受け取らず、戻り値もありません。その役割は、デシリアライズ後のオブジェクトを正常に利用できる状態に整えることです。
__wakeupは、unserialize()関数で文字列からオブジェクトが復元される際に自動的に実行されるマジックメソッドです。このメソッドは、自分で直接呼び出すものではありません。主な目的は、シリアライズ時に失われたデータベース接続やファイルハンドルのような「リソース」を再初期化することです。サンプルコードでは、serialize時に__sleepメソッドでファイルハンドルを意図的に破棄し、unserialize後の__wakeupメソッドで再度ファイルを開き直すことで、オブジェクトを再び利用可能な状態にしています。注意点として、ユーザー入力など信頼できないデータをunserialize()で処理すると、深刻なセキュリティ脆弱性につながる危険があるため、データの出所には常に注意が必要です。