【入門編】PHPの『シリアライズ』におけるオブジェクトインジェクションの根本対策:__wakeupと__unserializeの内部挙動 – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

こんにちは。日々、巨大なトラフィックをさばくWebシステムの設計に向き合っていることと思います。他の言語、例えばJavaやRuby、Node.jsなどのバックエンドを経験したエンジニアがPHPの世界に踏み込んだとき、誰もが一度は「マジックメソッド」や「シリアライズ」という概念の独特さに戸惑うのではないでしょうか。

特に `unserialize()`。この関数は、外部から受け取ったただの「文字列」を、一瞬にしてリッチなオブジェクトグラフへと復元してくれる非常に強力な機能です。しかし、ここを正しく理解していないと、アプリケーション全体を揺るがすセキュリティホール(オブジェクトインジェクション)を生み出す原因になってしまいます。

今回は、このデシリアライズの瞬間にZend VM(PHPの実行エンジン)の内部で何が起きているのか、そして新旧のライフサイクル(`__wakeup` と `__unserialize`)がメモリ上でどう処理されるのかを、低レイヤの視点も交えながら優しく紐解いていきましょう。ここを理解すれば、PHPの裏側がぐっと綺麗に見えてきますよ。

—

1. デシリアライズとは「メモリ空間の復元」である

まず、PHPの `serialize()` と `unserialize()` が内部で何をしているのかをイメージしてみましょう。

私たちが普段書くPHPのコードは、最終的にZend VMが理解する「オペコード(Opcode)」にコンパイルされ、メモリ上で実行されます。オブジェクトは、Zend Engineの内部構造体である `zend_object` としてヒープ上に存在し、プロパティの数や名前は `HashTable`(ハッシュテーブル)という高速な検索構造で管理されています。

`serialize()` は、このヒープ上の複雑なオブジェクト構造(参照関係やプロパティの型・値)を、一度「直列化されたバイト列(文字列)」にシリアライズします。そして `unserialize()` はその逆です。

[シリアライズ文字列] —> (unserialize) —> Zend VMがヒープ上にオブジェクトを再構築 (zend_object)

問題は、この「再構築のプロセス」にあります。
`unserialize()` は、文字列をパースしながら、どのクラスのインスタンスを生成し、どのプロパティにどんな値を代入すべきかを勝手に決定してメモリ上にオブジェクトを組み立ててしまうのです。もし、その文字列の構造を悪意ある第三者が巧妙に改ざんできたらどうなるでしょうか?

これが、PHPオブジェクトインジェクションの根本的なメカニズムです。

—

2. ガジェットチェーンが起動する瞬間:内部挙動のトレース

オブジェクトインジェクション単体では、ただ「意図しないクラスのインスタンスがメモリ上に作られる」だけに過ぎません。脅威となるのは、そのオブジェクトが破棄される時、あるいはデシリアライズされる瞬間に、PHPのマジックメソッドが自動的に呼び出される点です。

攻撃者は、アプリケーション内に既存するクラス群(サードパーティ製ライブラリやフレームワークのコードなど)の中から、デシリアライズの過程で都合よく連鎖的にメソッドを実行してくれるパーツ(これらをガジェットと呼びます)を探し出し、組み合わせます。これが「ガジェットチェーン」です。

Zend VMの視点で見ると、この連鎖は以下のように非情なまでに機械的に実行されます。

1. `unserialize()` がバイト列を読み込み、指定されたクラスのオブジェクトをインスタンス化する(この時点ではコンストラクタ `__construct()` は呼ばれません)。
2. プロパティに値が復元される。
3. ライフサイクルを制御するためのマジックメソッド(例: `__wakeup()`)がエンジンによって自動的にフックされ、コールされる。
4. そのメソッド内で、ファイルシステムへのアクセス、データベース操作、あるいは別のオブジェクトのメソッド呼び出しなどが連鎖的に発火する。

—

3. 旧時代の遺物 `__wakeup()` の限界

かつて、デシリアライズ時の安全性を担保するために `__wakeup()` メソッドが使われていました。オブジェクトが復元された直後に呼ばれるため、「ここでプロパティをサニタイズすればいいや」と考えてしまいがちです。

しかし、ここに大きな罠がありました。

class UserSession {
public $data;

// 古いアプローチ
public function __wakeup() {
// ここで検証を行おうとするが……
// すでにオブジェクト自体は生成されてしまっている!
$this->data = sanitize($this->data);
}
}

Zend VMの実行フローにおいて、`__wakeup()` が呼ばれた「直前の瞬間」には、すでに脆弱なプロパティを持ったオブジェクトがメモリ上に存在しています。もしガジェットチェーンがこの `__wakeup` や、あるいはオブジェクトがスコープを抜けて破棄される時の `__destruct()` をトリガーするように組まれていた場合、サニタイズ処理が走るよりも前に攻撃コードが実行されてしまうケースを防ぎきれないのです。

これが、`__wakeup()` がセキュリティ対策として信頼できない根本的な理由です。

—

4. 救世主:`__unserialize()` と `__serialize()` の内部仕様

PHP 7.4からは、この問題を根本から解決するために `Serializable` インターフェースが非推奨となり、代わりに新しいマジックメソッドである `__serialize()` と `__unserialize()` が導入されました。

この新しい仕組みが優れているのは、「オブジェクトの復元プロセスを完全に開発者の制御下に置ける」という点です。

実際のコードでその美しさと堅牢性を確認してみましょう。

class SecureSession implements Serializable { // 古いインターフェースは使わない!

private string $username;
private array $roles;

public function __construct(string $username, array $roles) {
$this->username = $username;
$this->roles = $roles;
}

/

  • PHP 7.4+ で導入された新しいシリアライズ制御
  • 保存すべき配列データのみを返す

/
public function __serialize(): array {
return [
‘username’ => $this->username,
‘roles’ => $this->roles,
];
}

/

  • デシリアライズ時にZend VMから直接呼び出される
  • @param array $data

/
public function __unserialize(array $data): void {
// 1. まず入力データの型や構造を厳密にバリデーションする
if (!isset($data[‘username’], $data[‘roles’]) || !is_string($data[‘username’])) {
throw new \InvalidArgumentException(‘不正なシリアライズデータが検出されました。’);
}

// 2. 安全が確認された値だけをプロパティにバインドする
$this->username = $data[‘username’];

// 権限昇格を防ぐため、ロールの型も厳格にチェック
$this->roles = array_filter($data[‘roles’], fn($role) => is_string($role));
}
}

この実装がなぜ安全なのか?

`__unserialize(array $data)` を実装した場合、Zend VMはオブジェクトのプロパティへ勝手に値を流し込むのではなく、デシリアライズされた生データ(配列)をそのままこのメソッドに渡します。

つまり、オブジェクトが完全に再構築される前に、あなたの手でデータの型や内容を完全に検証(バリデーション)し、不正なデータであれば例外を投げて処理を即座に中断できるのです。ガジェットが入り込む隙をメモリ上で完全に断つことができます。

—

5. アーキテクトからの実践的な提言

Webシステムを堅牢に保つための鉄則として、以下のプラクティスをチーム全体で共有してください。

1. 外部からの入力を信用して `unserialize()` しない
ユーザーからのリクエスト(Cookie、POSTパラメータ、データベースの未検証なカラムなど)を直接デシリアライズすることは、爆弾を抱えるようなものです。可能な限り JSON (`json_encode` / `json_decode`) を使用してください。JSONは単なる「データの表現」であり、オブジェクトのインスタンス化やコードの自動実行(マジックメソッドの呼び出し)を一切行わないため、構造的に安全です。

2. どうしてもシリアライズが必要なら `__unserialize` を強制する
レガシーなシステムなどでどうしてもオブジェクトのシリアライズ保存が必要な場合は、必ず `__serialize()` と `__unserialize()` を実装し、受け入れた配列データの厳格な型チェックを行ってください。クラス内のプロパティに直接代入するのではなく、ホワイトリスト方式でデータを精査するのが鉄則です。

3. 型宣言(Type Hinting)をフルに活用する
PHP 74以降、プロパティの型宣言が強力になっています。`__unserialize` 内でしっかりと型を縛ることで、Zend VMレベルでも期待したメモリ構造を維持しやすくなります。

—

PHPは、その動的な柔軟性ゆえに「魔法」のように動くコードを簡単に書くことができます。しかし、プロフェッショナルなWebアーキテクトとしてシステムを預かる私たちは、その「魔法」が内部のZend VMでどのように解釈され、メモリ上でどう処理されているのかを常に意識しなくてはなりません。

ここを理解したあなたなら、もうセキュリティの脅威に怯える必要はありません。ぜひ、セキュアで美しい設計をプロダクトに実装していってくださいね。

タイトルとURLをコピーしました