Laravel13への更新作業
古い PHP を使った Web システムの更新作業をご依頼いただくことがちょくちょくあります。 Laravel を使ったシステムですと、PHP のバージョンアップと共に Laravel も一緒にバージョンアップしなければならないことがあります。 今年3月に Laravel13 が出てから、Laravel13 への更新事例も出てきました。
Laravel12 から Laravel13 への差分の小さいアップグレードなら、母体のシステムがそれほど大きなものでなければアップグレードもすんなりいく印象です。
cache.serializable_classes 設定項目の追加
Laravel13 のアップグレードガイドによるといくつか影響度の大きい変更が出ていますが、その中の一つで config/cache.php に serializable_classes という設定が増えています。
Laravel のキャッシュでは PHP のオブジェクトもキャッシュに格納できるようになっていますが、この設定はキャッシュに PHP のクラスオブジェクトをキャッシュしていた場合のアンシリアライズ(unserialize)動作に影響します。 Laravel において、何らかのデータ(PHP オブジェクト型を含む)をキャッシュに保存する場合、データはキャッシュドライバ(RedisStore, MemcachedStore等)によってシリアライズされて保存されます。 逆にキャッシュから値を取得する際、データがアンシリアライズされて復元されます。 serializable_classes は PHP オブジェクトをアンシリアライズする際、本設定で指定した型のオブジェクトのみアンシリアライズするというものです。
未許可のクラスオブジェクトをアンシリアライズするとどうなるか?
serializable_classes で指定していない型のクラスをキャッシュに格納していた場合はどうなるのでしょうか?
以下のようなキャッシュ処理があるとします。これはキャッシュ(Key は 'foo')から Member オブジェクトの取得を試みて、キャッシュになければ新規にキャッシュに保存しています。
キャッシュ処理の例
class Member
{
public $name = 'foo';
}
function test()
{
$value = Cache::remember('foo', 60, function () {
return new Member();
});
print_r($value);
}
serializable_classes が null に設定されていた場合、あらゆる型の PHP オブジェクトのアンシリアライズが許可されます。この場合、キャッシュに格納されていた Member オブジェクトは以下のように正常に復元されます。
App\Console\Commands\Member Object
(
[name] => foo
)
一方、serializable_classes が false に設定されていた場合、あらゆる型の PHP オブジェクトのアンシリアライズが拒否されます。結果としては、Member オブジェクトが __PHP_Incomplete_Class オブジェクトに変換されて返されます。
__PHP_Incomplete_Class Object
(
[__PHP_Incomplete_Class_Name] => App\Console\Commands\Member
[name] => foo
)
実装の話になりますが、Laravel においてキャッシュの処理はキャッシュ種別(Redis, Memcached, etc.)に応じたキャッシュドライバで処理されます。キャッシュドライバには以下のようなものがあります。
- Illuminate\Cache\RedisStore
- Illuminate\Cache\MemcachedStore
キャッシュドライバの実装によりますが、serializable_classes の機能は、各キャッシュドライバがアンシリアライズを PHP の unserialize() 関数で処理することを前提とした機能です。 例えば Redis のキャッシュドライバである Illuminate\Cache\RedisStore も unserialize() 関数を使ってキャッシュ済みデータをアンシリアライズしています。
一方、PHP の unserialize() 関数には allowed_classes オプションというものがあり、このオプションでアンシリアライズ対象のクラスを制限できるようになっています。受付対象外のクラスをアンシリアライズしようとした場合は、__PHP_Incomplete_Class に変換されます。 serializable_classes で指定した config 値はキャッシュドライバを経由して最終的に unserialize() の allowed_classes オプションに渡されるので、Laravel のキャッシュにおいても__PHP_Incomplete_Class が返されるというわけです。
このように、既存のコードで PHP オブジェクトをキャッシュに保存する処理があった場合、serializable_classes の設定によっては、キャッシュ取得時の型が変わってしまうため注意が必要です。
どう修正すべきか
この変更を取り込む前にシステムの動作を確認しておく必要があります。 とれる方法は以下の3つになります。
(1) 未定義とする
cache.php は修正せず serializable_classes は未定義のままとする。もしくはnullを設定する。
この場合は、unserialize()関数のallowed_classesオプションは指定されないので、Laravel12までと同じ動作となります。
(2) 明示的にクラスを指定する(false指定含む)
キャッシュしているオブジェクトの型をすべて列挙する。もしくは false を指定する。 falseを指定した場合は、すべてのPHPオブジェクトのアンシリアライズを許さない。 PHP オブジェクトはすべて __PHP_Incomplete_Class に変換される。
キャッシュにどのようなデータを格納しているのかを完全に把握できているのなら、この設定がベストになります。
(3) trueを指定する
すべてのPHPオブジェクトをアンシリアライズ可能にする。
これは(1)と同じ動作になります。
新規に開発するなら厳密に指定するのがいいのですが(*1)、 旧バージョンからのバージョンアップで現状のキャッシュ内容に確証が持てないのなら、とりあえずは(1),(3)とするのがいいでしょう。
Laravel13 のデフォルトの config/cache.php から serializable_classes の設定をコピペして false 設定のままでオブジェクトをキャッシュしているとキャッシュからの取得時に型が化けるため、テスト時に「何かおかしい。。。」ということになりますので注意が必要です。
Memcachedを使ったキャッシュでは無視される
余談ですが、キャッシュデータのアンシリアライズは各キャッシュドライバで行っているといいました。Memcached のキャッシュドライバである Illuminate\Cache\MemcachedStore ではアンシリアライズに unserialize() を使用していないため serializable_classes の設定は無視されます。
Memcached のキャッシュ処理の実体は PHP のmemcached 拡張で処理されており(MemcachedStore は memcached 拡張に転送しているだけ)、アンシリアライズ処理も memcached 拡張内で処理され Laravel が介入できないためです。
元々なんのための機能なのか
serializable_classes 設定は元々なんのために追加された機能なのでしょうか?
gitのlog(commit bd6972d0)には以下のようにあります。
support a serializable classes value on caches
PHP オブジェクトをキャッシュできるようにしたということのようです。 厳密には今までにも PHP オブジェクトをキャッシュすることは可能でしたが、 allowed_classes オプションを指定できるようにして少しでも Gadget Chain 攻撃のリスクを小さくできる形で明確にサポートしたということでしょう。
ただし、serializable_classes による制限設定ができたとしても、PHP オブジェクトを unserialize する場合は「信用できるデータ以外はアンシリアライズしない」という原則は守る必要があります。PHP オブジェクトをキャッシュする際は、それが適切に構築されたオブジェクトであるかに注意を払うようにしましょう。 unserialize() 関数のリファレンスにも以下の記述があります。
警告
options の allowed_classes の値にかかわらず、 ユーザーからの入力をそのまま unserialize() に渡してはいけません。 アンシリアライズの時には、オブジェクトのインスタンス生成やオートローディングなどで コードが実行されることがあり、悪意のあるユーザーがこれを悪用するかもしれないからです。 シリアル化したデータをユーザーに渡す必要がある場合は、安全で標準的なデータ交換フォーマットである JSON などを使うようにしましょう。 json_decode() および json_encode() を利用します。
APP_KEY 漏洩時の Gadget Chain 攻撃対策とあるが
また、config/cache.php の serializable_classes 項目には以下のようなコメントがあります。
By default, no PHP classes will be unserialized from your cache to prevent gadget chain attacks if your APP_KEY is leaked.
デフォルトでは、APP_KEYが漏洩した場合のガジェットチェーン攻撃を防ぐため、キャッシュからPHPクラスのシリアライズは行われません。
APP_KEY 漏洩時の Gadget Chain 攻撃対策の目的もあるようですが、この点をちょっと補足しておきます。
Laravel では Cookie の内容は暗号化され HMAC による偽造のチェックも行われるようになっています。この仕組みにより、Cookie の内容はサーバー外部に対して秘匿され偽造も不可能になり、 セッションデータを Cookie に保存する(*2)こともできるようになっています。 ただし、APP_KEY が漏洩した場合、本来サーバー側でしか作成できなかったCookie値を外部(クライアント側)でも作成できるようになってしまうため、セッションデータを Cookie に保存していた場合、セッションデータを外部から自由に操作できるようになってしまいます。
そこで、APP_KEY が漏洩した時点で以下の条件が揃っていると、外部から任意のデータ(偽造したCookie値)を unserialize() に渡すことが可能になるのでGadget Chain 攻撃が成立しやすくなります。
- session.driver (SESSION_DRIVER) 設定が 'cookie' となっている
- session.serialization 設定が 'php' となっている(*3)
上記コメントによると serializable_classes を設定すれば Gadget Chain 攻撃を防げそうですが、このケースでのアンシリアライズ処理は Illuminate\Session\Store で処理されるもので、キャッシュは関係ない(*4)ため実際には効果はありません。
session.driver が 'redis' とかのキャッシュを使うようになっていた場合では、APP_KEY が漏洩しても、そもそもセッションデータを外部から自由に操作することはできないので、 コメントにあるような APP_KEY漏洩 → Cookie偽造 → Gadget Chain攻撃 という流れが成立しないのではないかと考えられます。
ということで、serializable_classes の設定は APP_KEY 漏洩時の Gadget Chain攻撃への対策としては効果はないのでは?というのが個人的な見解です。
ただし、unserialize()の呼び出しにはリスクが伴うというのは事実なので、serializable_classes を指定できるなら指定しておいた方がいいとは思います。キャッシュサーバーが汚染されたり別の不具合で、想定していない PHP オブジェクトをアンシリアライズさせられるというケースはあるかもしれません。
(*1) serializable_classes は false にして、オブジェクトのキャッシュは一切しないのが最も安全ではあります。
(*2) session.driver 設定を'cookie'にする。
(*3) session.serialization も Laravel13 で追加された設定。未指定の場合、'php' が指定される。
(*4) このケースではセッションデータはRedisとかのキャッシュではなくCookieに保存しているのでそもそもキャッシュは関係ない。
投稿日:2026/07/28 00:20(最終更新:2026/07/29 01:05)