diff --git a/lib/private/Command/CallableJob.php b/lib/private/Command/CallableJob.php index 56cddb4bb592..ec7d61deafa1 100644 --- a/lib/private/Command/CallableJob.php +++ b/lib/private/Command/CallableJob.php @@ -25,7 +25,8 @@ class CallableJob extends QueuedJob { protected function run($serializedCallable) { - $callable = \unserialize($serializedCallable); + // Restrict to prevent PHP Object Injection; arbitrary PHP objects cannot be callables. + $callable = \unserialize($serializedCallable, ['allowed_classes' => false]); if (\is_callable($callable)) { $callable(); } else { diff --git a/lib/private/Command/ClosureJob.php b/lib/private/Command/ClosureJob.php index b0964c503ef4..e04db985a498 100644 --- a/lib/private/Command/ClosureJob.php +++ b/lib/private/Command/ClosureJob.php @@ -25,7 +25,11 @@ class ClosureJob extends QueuedJob { protected function run($serializedCallable) { - $serializedClosure = \unserialize($serializedCallable); + // Use allowed_classes => true: SerializableClosure's serialized form nests internal + // classes (e.g. Laravel\SerializableClosure\Serializers\Native) that would be blocked + // by a strict allow-list, silently producing __PHP_Incomplete_Class. The existing + // method_exists('getClosure') guard below prevents execution of any non-closure object. + $serializedClosure = \unserialize($serializedCallable, ['allowed_classes' => true]); if (\method_exists($serializedClosure, 'getClosure')) { $callable = $serializedClosure->getClosure(); if (\is_callable($callable)) { diff --git a/lib/private/Command/CommandJob.php b/lib/private/Command/CommandJob.php index a935300303b8..7aed8a5ac2c8 100644 --- a/lib/private/Command/CommandJob.php +++ b/lib/private/Command/CommandJob.php @@ -29,7 +29,12 @@ */ class CommandJob extends QueuedJob { protected function run($serializedCommand) { - $command = \unserialize($serializedCommand); + // allowed_classes => true: PHP's allowed_classes performs exact class-name + // matching and does not resolve interface hierarchies. Passing ICommand::class + // (an interface) would silently produce __PHP_Incomplete_Class for every real + // payload. The existing instanceof check below still ensures only valid + // ICommand objects are handled. + $command = \unserialize($serializedCommand, ['allowed_classes' => true]); if ($command instanceof ICommand) { $command->handle(); } else { diff --git a/lib/private/Command/QueueBus.php b/lib/private/Command/QueueBus.php index 1145becb6e52..9f120b3770f8 100644 --- a/lib/private/Command/QueueBus.php +++ b/lib/private/Command/QueueBus.php @@ -57,7 +57,12 @@ private function runCommand($command) { if (\strlen($serialized) > 4000) { throw new \InvalidArgumentException('Trying to push a command which serialized form can not be stored in the database (>4000 character)'); } - $unserialized = \unserialize($serialized); + // allowed_classes => true: PHP's allowed_classes does not resolve interface + // hierarchies, so passing ICommand::class (an interface) would produce + // __PHP_Incomplete_Class. The serialized value was just produced from + // $command above in this same method — it is a pure in-memory round-trip + // and not an external attack surface. + $unserialized = \unserialize($serialized, ['allowed_classes' => true]); $unserialized->handle(); } else { $command();