はじめに
PHPでは、配列やオブジェクトをファイルやデータベース、セッションに保存する際に、serialize() を使って文字列形式に変換することがよくあります。そして、その文字列を元のデータ構造に戻すために使うのが unserialize() 関数です。
unserialize() は非常に便利な関数である一方、信頼できない入力を渡すと深刻なセキュリティリスク(PHPオブジェクトインジェクション)につながるという、他の多くの関数とは一線を画す注意点を持っています。本記事では、基本的な使い方から、安全に使うためのallowed_classesオプションの活用法、そして実務でよくあるユースケースまで、実践的なコード例とともに詳しく解説します。
関数概要
| 項目 | 内容 |
|---|---|
| 関数名 | unserialize() |
| 所属拡張 | コア関数(標準で常に利用可能) |
| シグネチャ | unserialize(string $data, array $options = []): mixed |
| 引数1 | $data — シリアライズされた文字列 |
| 引数2 | $options — allowed_classes などのオプション配列 |
| 戻り値 | 復元されたPHPの値。失敗時は false |
| 対応バージョン | PHP 4以降(allowed_classes オプションはPHP 7.0以降) |
| 対になる関数 | serialize() |
| 重要な注意点 | 信頼できない入力の復元にはセキュリティリスクが伴う |
復元の流れとセキュリティリスク(イメージ図)
シリアライズ文字列
'O:8:"MyClass":1:{s:4:"name";s:3:"Bob";}'
│
▼
unserialize()
│
┌──────────┴──────────────────┐
│ 文字列を解析し、対応するクラスの │
│ インスタンスとして復元しようとする │
└──────────┬──────────────────┘
▼
┌───────────────────────────────┐
│ 危険なパターン: │
│ 復元時に __wakeup() や __destruct() │
│ などのマジックメソッドが自動的に呼ばれる │
│ → 悪意あるクラスが仕込まれていると │
│ 任意コード実行につながる可能性がある │
└───────────────────────────────┘
ポイントは、unserialize() が文字列中に埋め込まれたクラス名の情報をもとに、任意のクラスのインスタンスを生成しようとするという挙動です。もし攻撃者がシリアライズ文字列を細工し、__wakeup() や __destruct() などのマジックメソッドを持つ危険なクラスを指定できてしまうと、意図しない処理が実行される「PHPオブジェクトインジェクション」という脆弱性につながります。信頼できない外部入力(ユーザー入力、Cookie、外部APIレスポンスなど)を絶対にそのままunserialize()してはいけません。
実践サンプル7選
例1:基本的な使い方
<?php
class BasicUnserializer
{
public function restore(string $serialized): mixed
{
// シリアライズされた文字列を元のPHPの値に復元する
return unserialize($serialized);
}
}
$unserializer = new BasicUnserializer();
$original = ['name' => '太郎', 'age' => 30];
$serialized = serialize($original);
print_r($unserializer->restore($serialized));
// ['name' => '太郎', 'age' => 30]
例2:allowed_classesオプションでセキュリティを確保する
<?php
class SecureUnserializer
{
/**
* allowed_classesをfalseにすることで、
* オブジェクトを一切復元させず、配列やスカラー値のみを許可する
*/
public function restoreDataOnly(string $serialized): mixed
{
return unserialize($serialized, ['allowed_classes' => false]);
}
/**
* 特定の安全なクラスのみを許可するホワイトリスト方式
*/
public function restoreWithAllowedClasses(string $serialized, array $allowedClasses): mixed
{
return unserialize($serialized, ['allowed_classes' => $allowedClasses]);
}
}
$secure = new SecureUnserializer();
$data = serialize(['id' => 1, 'status' => 'active']);
var_dump($secure->restoreDataOnly($data));
例3:外部システムから受け取ったキャッシュデータの安全な復元
<?php
class CacheReader
{
/**
* キャッシュストアから取得した文字列を、
* 許可されたDTOクラスのみを対象に復元する
*/
public function read(string $cachedValue): mixed
{
// 自社が生成したデータ用のDTOクラスのみを許可する
$result = unserialize($cachedValue, ['allowed_classes' => [CacheEntryDto::class]]);
if ($result === false && $cachedValue !== serialize(false)) {
throw new RuntimeException('キャッシュデータの復元に失敗しました');
}
return $result;
}
}
class CacheEntryDto
{
public function __construct(public string $key, public mixed $value, public int $expiresAt)
{
}
}
$reader = new CacheReader();
$cached = serialize(new CacheEntryDto('user:1', ['name' => '花子'], time() + 3600));
$entry = $reader->read($cached);
print_r($entry);
例4:復元失敗を正しく検知するエラーハンドリング
<?php
class SafeDataRestorer
{
/**
* unserialize()は失敗時にfalseを返すが、
* "false"という値自体をシリアライズした場合と区別がつかない問題への対処
*/
public function restore(string $serialized): array
{
// set_error_handler()で警告をキャッチしてエラー扱いにする
set_error_handler(function () {
throw new RuntimeException('unserializeで警告が発生しました');
});
try {
$result = unserialize($serialized, ['allowed_classes' => false]);
return ['success' => true, 'data' => $result];
} catch (Throwable $e) {
return ['success' => false, 'error' => $e->getMessage()];
} finally {
restore_error_handler();
}
}
}
$restorer = new SafeDataRestorer();
print_r($restorer->restore('a:2:{i:0;s:1:"a";i:1;s:1:"b";}'));
print_r($restorer->restore('不正なシリアライズ文字列'));
例5:セッションデータのバックアップと復元
<?php
class SessionBackupManager
{
/**
* セッション変数をシリアライズしてファイルに保存し、
* 必要なときに復元する(同一システム内での利用を想定)
*/
public function backup(array $sessionData, string $filePath): void
{
file_put_contents($filePath, serialize($sessionData));
}
public function restore(string $filePath): array
{
if (!file_exists($filePath)) {
return [];
}
$content = file_get_contents($filePath);
// 自システム内で生成したデータのみを対象とするため配列限定で復元
$data = unserialize($content, ['allowed_classes' => false]);
return is_array($data) ? $data : [];
}
}
$manager = new SessionBackupManager();
$manager->backup(['user_id' => 42, 'cart' => ['item1', 'item2']], '/tmp/session_backup.dat');
print_r($manager->restore('/tmp/session_backup.dat'));
例6:危険な入力を検証してから復元する多層防御の実装
<?php
class DefenseInDepthUnserializer
{
/**
* 複数の防御層を組み合わせて安全性を高める:
* 1. オブジェクト指定子("O:")を含む文字列を事前に拒否
* 2. allowed_classes=falseで念のため二重に防御
*/
public function restore(string $serialized): mixed
{
// シリアライズ文字列中に "O:"(オブジェクト)や "C:"(カスタムシリアライズ)
// が含まれる場合は処理を拒否する簡易チェック
if (preg_match('/[OC]:\d+:/', $serialized)) {
throw new InvalidArgumentException('オブジェクトを含むデータは許可されていません');
}
return unserialize($serialized, ['allowed_classes' => false]);
}
}
$defender = new DefenseInDepthUnserializer();
try {
print_r($defender->restore('a:1:{i:0;s:5:"hello";}'));
print_r($defender->restore('O:8:"stdClass":0:{}')); // 例外がスローされる
} catch (InvalidArgumentException $e) {
echo 'ブロックされました: ' . $e->getMessage() . PHP_EOL;
}
例7:JSONへの移行を推奨する比較実装
<?php
class SerializationFormatComparator
{
/**
* 外部との連携や信頼できない入力の受け渡しには
* unserialize()よりもjson_decode()の方が安全であることを示す比較
*/
public function demonstrateSafety(array $data): array
{
$serialized = serialize($data);
$json = json_encode($data);
return [
// json_decode()はオブジェクトインジェクションのリスクがない
'json_decode_result' => json_decode($json, true),
// unserialize()は同じ用途でもリスクを伴う
'unserialize_result' => unserialize($serialized, ['allowed_classes' => false]),
];
}
}
$comparator = new SerializationFormatComparator();
print_r($comparator->demonstrateSafety(['id' => 1, 'tags' => ['php', 'security']]));
関連関数との比較
| 関数 | 役割 | unserializeとの違い |
|---|---|---|
unserialize() | シリアライズ文字列をPHPの値に復元 | 本記事の対象。オブジェクトも復元できる分リスクが高い |
serialize() | PHPの値をシリアライズ文字列に変換 | unserialize() の対になるエンコード関数 |
json_decode() | JSON文字列をPHPの値に変換 | オブジェクトインジェクションのリスクがなく、外部連携ではこちらが推奨される |
json_encode() | PHPの値をJSON文字列に変換 | serialize() のJSON版であり、言語間の互換性が高い |
var_export() | 変数をPHPコードとして出力可能な文字列に変換 | 復元にはPHPコードとしてのevalやインクルードが必要で用途が異なる |
よくある落とし穴(注意点)
- 信頼できない入力を絶対に渡さない ユーザーが操作可能なCookie、GET/POSTパラメータ、外部APIレスポンスなどを直接
unserialize()に渡すと、PHPオブジェクトインジェクションによる深刻な脆弱性につながります。これがunserialize()に関する最も重要な注意点です。 allowed_classesオプションを省略しない PHP 7.0以降で導入されたallowed_classesオプションを指定しない場合、デフォルトではすべてのクラスの復元が許可されてしまいます。特別な理由がない限り、false(オブジェクト復元を禁止)または許可クラスのホワイトリストを明示的に指定しましょう。- 戻り値の
falseと、値としてのfalseを区別できない 復元に失敗した場合もfalseが返りますが、serialize(false)の復元結果もfalseになるため、単純な=== falseの判定だけでは失敗を正確に検知できません(例4のようなエラーハンドラとの併用が有効です)。 - 外部連携にはJSON形式を優先する 異なるシステム間でデータをやり取りする場合、
serialize()/unserialize()はPHP固有の形式であるため、json_encode()/json_decode()を使う方が安全性・互換性の両面で優れています(例7を参照)。 - 大きなデータ構造のパフォーマンス 非常に大きな配列やオブジェクトをシリアライズ・復元すると、メモリ使用量や処理時間が増大します。大規模データにはデータベースや専用のシリアライズフォーマット(Protocol Buffersなど)の利用も検討する価値があります。
まとめ
| 観点 | まとめ |
|---|---|
| 何をする関数か | serialize() で作成された文字列を元のPHPの値(配列・オブジェクトなど)に復元する |
| 主な用途 | キャッシュデータの復元、セッションバックアップ、自システム内でのデータ永続化 |
| 最重要の注意点 | 信頼できない入力を渡さない。PHPオブジェクトインジェクションのリスクがある |
| 安全に使う方法 | allowed_classes オプションを必ず指定する(falseまたは明示的なホワイトリスト) |
| 代替の選択肢 | 外部連携や信頼できない入力の処理にはjson_decode()が推奨される |
unserialize() はPHPの値を柔軟に復元できる強力な関数ですが、その柔軟性ゆえに重大なセキュリティリスクを内包しています。「信頼できるデータにのみ使う」「allowed_classesを必ず指定する」という2つの原則を徹底し、可能であれば外部連携にはJSON形式を優先することを強くおすすめします。
