【PHP8.x】DOMEntityReference::__wakeup()メソッドの使い方
__wakeupメソッドの使い方について、初心者にもわかりやすく解説します。
基本的な使い方
__wakeupメソッドは、シリアライズされたDOMEntityReferenceオブジェクトの復元(デシリアライズ)を禁止する処理を実行するメソッドです。PHPには、オブジェクトの状態を保存や転送が可能な文字列形式に変換する「シリアライズ」と、その文字列から元のオブジェクトを復元する「デシリアライズ」という仕組みがあります。__wakeupは、unserialize()関数によってオブジェクトが復元される際に自動的に呼び出される、PHPの特別なマジックメソッドの一つです。DOMEntityReferenceオブジェクトは、XMLドキュメント全体の構造と密接に関連しているため、単純な文字列からの復元は想定されていません。もしデシリアライズを許可してしまうと、オブジェクトの内部状態が不整合を起こし、プログラムの予期せぬエラーや不正な動作に繋がる危険性があります。この問題を未然に防ぐため、DOMEntityReferenceクラスの__wakeupメソッドは、デシリアライズ処理を意図的に失敗させる役割を担います。具体的には、このメソッドが呼び出されると、処理を中断して必ずDOMExceptionという例外を発生させます。これにより、開発者に対してこの操作がサポートされていないことを明確に通知し、アプリケーションの安定性を保護します。
構文(syntax)
1public function __wakeup(): void;
引数(parameters)
引数なし
引数はありません
戻り値(return)
void
__wakeupメソッドは、オブジェクトがシリアライズ解除された後に呼び出されます。このメソッドの戻り値はありません。
サンプルコード
__wakeup()バイパス脆弱性のデモ
1<?php 2 3/** 4 * __wakeup() メソッドの挙動と、過去に存在したバイパス脆弱性(CVE-2016-7124)を 5 * 確認するためのデモクラスです。 6 */ 7class WakeupBypassDemo 8{ 9 // オブジェクトのプロパティ 10 public string $message; 11 12 public function __construct(string $message = 'Initialized') 13 { 14 $this->message = $message; 15 } 16 17 /** 18 * unserialize() によってオブジェクトが復元される際に自動的に呼び出されるマジックメソッド。 19 * リソースの再接続や、オブジェクトの状態の検証などに使用されます。 20 */ 21 public function __wakeup(): void 22 { 23 echo "__wakeup() メソッドが呼び出されました。\n"; 24 $this->message = 'Woken up!'; 25 } 26} 27 28// 正常なオブジェクトを生成し、シリアライズします。 29$object = new WakeupBypassDemo('Original message'); 30$serializedString = serialize($object); 31 32echo "元のシリアライズ文字列:\n"; 33echo $serializedString . "\n\n"; 34 35// シリアライズされた文字列を改ざんし、__wakeup() のバイパスを試みます。 36// これは、オブジェクトに宣言されているプロパティ数と、シリアライズ文字列内の 37// プロパティ数を意図的に食い違わせる攻撃手法でした。 38// O:16:"WakeupBypassDemo":1:{...} -> O:16:"WakeupBypassDemo":2:{...} 39$maliciousString = str_replace( 40 'O:16:"WakeupBypassDemo":1:', 41 'O:16:"WakeupBypassDemo":2:', 42 $serializedString 43); 44 45echo "改ざんされたシリアライズ文字列(プロパティ数を2に偽装):\n"; 46echo $maliciousString . "\n\n"; 47 48echo "改ざんされた文字列のデシリアライズを試みます...\n"; 49 50// PHP 7.1 以降(PHP 8 を含む)では、この脆弱性は修正されています。 51// プロパティ数が一致しないため、unserialize() は false を返し、警告(Warning)を発生させます。 52// その結果、__wakeup() は呼び出されません。 53// これはバイパスが成功したのではなく、デシリアライズ自体が安全に失敗したことを意味します。 54$result = unserialize($maliciousString); 55 56echo "デシリアライズ結果:\n"; 57var_dump($result); 58 59// 比較のため、正常な文字列をデシリアライズした場合の動作も確認します。 60echo "\n--- (比較) 正常なデシリアライズ ---\n"; 61$restoredObject = unserialize($serializedString); 62var_dump($restoredObject); 63 64?>
__wakeupは、unserialize()関数によって文字列からオブジェクトが復元される際に、自動的に呼び出されるマジックメソッドです。このメソッドは引数を持たず、戻り値もありません(void)。オブジェクトが復元された直後に、データベース接続を再確立するなどの初期化処理を行いたい場合に使用されます。
このサンプルコードは、__wakeupメソッドの動作と、過去に存在したセキュリティ上の脆弱性(CVE-2016-7124)を解説するものです。まず、オブジェクトをserialize()関数で文字列に変換します。次に、その文字列データに含まれるプロパティの数を、実際の数とは異なる値に意図的に改ざんします。
過去のPHPバージョンでは、このような不正なデータを使ってオブジェクトを復元すると、__wakeupメソッドの実行を回避(バイパス)できてしまう問題がありました。しかし、現在のPHP 8ではこの脆弱性は修正されています。そのため、サンプルコードのようにプロパティ数が一致しないデータをunserialize()関数に渡すと、処理は安全に失敗してfalseを返し、__wakeupメソッドが呼び出されることはありません。これは、不正なデータによる意図しない動作を防ぐための重要なセキュリティ対策です。
__wakeupは、unserialize()関数でオブジェクトが復元される際に自動で呼ばれる特殊なメソッドです。このサンプルコードは、過去のPHPに存在した、シリアライズ文字列を改ざんして__wakeupの実行を回避できてしまう脆弱性を再現しようとしています。しかし、現在のPHP 8ではこの脆弱性は修正済みであり、コードのように改ざんされたデータを渡すとデシリアライズは安全に失敗します。最も重要な注意点は、信頼できない外部からのデータをunserialize()することは、たとえこの脆弱性がなくても別の深刻なセキュリティ問題に繋がる危険性が高いため、原則として避けるべきという点です。安全なデータのやり取りには、JSON形式などを利用することを推奨します。
PHP __wakeup メソッドの動作を実演する
1<?php 2 3declare(strict_types=1); 4 5/** 6 * __wakeup マジックメソッドの動作を示すサンプルクラス 7 * 8 * オブジェクトがデシリアライズ(unserialize)された後に、 9 * 自動的に呼び出される __wakeup メソッドの挙動を実演します。 10 * データベース接続のような、シリアライズできないリソースを 11 * 再初期化するような場面で利用されます。 12 */ 13class ConnectionHandler 14{ 15 // データベース接続情報など、シリアライズしたいプロパティ 16 private string $dsn; 17 18 // 接続状態を示すプロパティ(リソースの代用) 19 // unserialize 後に再初期化する必要がある 20 private string $status; 21 22 public function __construct(string $dsn) 23 { 24 $this->dsn = $dsn; 25 $this->connect(); 26 } 27 28 /** 29 * 接続処理を模倣し、状態を 'CONNECTED' にします。 30 */ 31 private function connect(): void 32 { 33 // 実際にはここでデータベース接続などの処理を行う 34 $this->status = 'CONNECTED'; 35 echo "接続を確立しました。 (DSN: {$this->dsn})\n"; 36 } 37 38 /** 39 * 現在の接続状態を返します。 40 */ 41 public function getStatus(): string 42 { 43 return $this->status; 44 } 45 46 /** 47 * serialize() 実行時に呼び出されます。 48 * 保存したいプロパティ名の配列を返します。 49 * ここでは、一時的な状態である $status を除外します。 50 */ 51 public function __sleep(): array 52 { 53 echo "__sleep() が呼び出されました。接続状態を保存対象から除外します。\n"; 54 return ['dsn']; 55 } 56 57 /** 58 * unserialize() 実行後に呼び出されます。 59 * このメソッドで、オブジェクトの復元に必要な初期化処理(リソースの再接続など)を行います。 60 */ 61 public function __wakeup(): void 62 { 63 echo "__wakeup() が呼び出されました。\n"; 64 $this->connect(); // 接続を再確立する 65 } 66} 67 68// 1. オブジェクトを生成します。コンストラクタで connect() が呼ばれます。 69echo "--- 1. オブジェクト生成 ---\n"; 70$handler = new ConnectionHandler('mysql:host=localhost;dbname=test'); 71echo "生成後の状態: " . $handler->getStatus() . "\n\n"; 72 73// 2. オブジェクトをシリアライズ(文字列に変換)します。 74// このとき __sleep() が呼び出されます。 75echo "--- 2. シリアライズ ---\n"; 76$serializedData = serialize($handler); 77echo "シリアライズされたデータ: " . $serializedData . "\n\n"; 78 79// 3. シリアライズされたデータからオブジェクトを復元します。 80// このとき、まずオブジェクトが復元され、その直後に __wakeup() が呼び出されます。 81echo "--- 3. デシリアライズ ---\n"; 82/** @var ConnectionHandler $unserializedHandler */ 83$unserializedHandler = unserialize($serializedData); 84echo "復元後の状態: " . $unserializedHandler->getStatus() . "\n"; 85 86?>
PHPの__wakeupメソッドは、オブジェクトのデシリアライズ時に自動的に呼び出される「マジックメソッド」の一つです。unserialize()関数によって、シリアライズされた文字列からオブジェクトが復元された直後に、このメソッドが実行されます。
このメソッドの主な目的は、オブジェクトの状態を完全に復元することです。例えば、データベース接続やファイルハンドルといったリソースは、シリアライズ(文字列への変換)の際にその接続状態を維持できません。そこで__wakeupメソッド内に、これらのリソースを再接続・再初期化する処理を記述することで、デシリアライズ後のオブジェクトをすぐに利用可能な状態に戻すことができます。
__wakeupメソッドは引数を受け取らず、またvoidとして定義されているため、何も値を返しません。
サンプルコードでは、ConnectionHandlerクラスのオブジェクトをserialize()で文字列に変換し、その後unserialize()で復元しています。unserialize()が実行されると__wakeupメソッドが自動で呼び出され、その中でconnect()メソッドが再度実行されます。これにより、シリアライズの過程で失われた接続状態が再確立され、オブジェクトが完全に復元される様子が示されています。このように__wakeupは、オブジェクトの復元処理を定義するために使用されます。
__wakeupメソッドは、unserialize()関数でオブジェクトが復元された直後に自動で呼び出されます。この時、コンストラクタ(__construct)は実行されない点に注意が必要です。主な用途は、サンプルコードのようにデータベース接続やファイルハンドルなど、シリアライズ(保存)できないリソースを再初期化することです。多くの場合、シリアライズ時に保存するプロパティを選択する__sleepメソッドと対で利用されます。最後に、信頼できない文字列をunserialize()すると、意図しないコードが実行される深刻なセキュリティ問題に繋がるため、必ず信頼できるデータに対してのみ使用してください。