[PHP]unserializeとは?シリアライズ文字列を安全に復元する方法とセキュリティ対策を徹底解説

PHP

はじめに

PHPでは、配列やオブジェクトをファイルやデータベース、セッションに保存する際に、serialize() を使って文字列形式に変換することがよくあります。そして、その文字列を元のデータ構造に戻すために使うのが unserialize() 関数です。

unserialize() は非常に便利な関数である一方、信頼できない入力を渡すと深刻なセキュリティリスク(PHPオブジェクトインジェクション)につながるという、他の多くの関数とは一線を画す注意点を持っています。本記事では、基本的な使い方から、安全に使うためのallowed_classesオプションの活用法、そして実務でよくあるユースケースまで、実践的なコード例とともに詳しく解説します。


関数概要

項目内容
関数名unserialize()
所属拡張コア関数(標準で常に利用可能)
シグネチャunserialize(string $data, array $options = []): mixed
引数1$data — シリアライズされた文字列
引数2$optionsallowed_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やインクルードが必要で用途が異なる

よくある落とし穴(注意点)

  1. 信頼できない入力を絶対に渡さない ユーザーが操作可能なCookie、GET/POSTパラメータ、外部APIレスポンスなどを直接 unserialize() に渡すと、PHPオブジェクトインジェクションによる深刻な脆弱性につながります。これが unserialize() に関する最も重要な注意点です。
  2. allowed_classes オプションを省略しない PHP 7.0以降で導入された allowed_classes オプションを指定しない場合、デフォルトではすべてのクラスの復元が許可されてしまいます。特別な理由がない限り、false(オブジェクト復元を禁止)または許可クラスのホワイトリストを明示的に指定しましょう。
  3. 戻り値のfalseと、値としてのfalseを区別できない 復元に失敗した場合も false が返りますが、serialize(false) の復元結果も false になるため、単純な === false の判定だけでは失敗を正確に検知できません(例4のようなエラーハンドラとの併用が有効です)。
  4. 外部連携にはJSON形式を優先する 異なるシステム間でデータをやり取りする場合、serialize()/unserialize() はPHP固有の形式であるため、json_encode()/json_decode() を使う方が安全性・互換性の両面で優れています(例7を参照)。
  5. 大きなデータ構造のパフォーマンス 非常に大きな配列やオブジェクトをシリアライズ・復元すると、メモリ使用量や処理時間が増大します。大規模データにはデータベースや専用のシリアライズフォーマット(Protocol Buffersなど)の利用も検討する価値があります。

まとめ

観点まとめ
何をする関数かserialize() で作成された文字列を元のPHPの値(配列・オブジェクトなど)に復元する
主な用途キャッシュデータの復元、セッションバックアップ、自システム内でのデータ永続化
最重要の注意点信頼できない入力を渡さない。PHPオブジェクトインジェクションのリスクがある
安全に使う方法allowed_classes オプションを必ず指定する(falseまたは明示的なホワイトリスト)
代替の選択肢外部連携や信頼できない入力の処理にはjson_decode()が推奨される

unserialize() はPHPの値を柔軟に復元できる強力な関数ですが、その柔軟性ゆえに重大なセキュリティリスクを内包しています。「信頼できるデータにのみ使う」「allowed_classesを必ず指定する」という2つの原則を徹底し、可能であれば外部連携にはJSON形式を優先することを強くおすすめします。

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