Skip to content

Add allowed_classes_callback to unserialize() #19087

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

langemeijer
Copy link

I want to propose an added allowed_classes_callback option to unserialize()

The class name as parsed from the serialized string is passed as a parameter to the callback, it should return a boolean. true would allow the class, false would block it the same way 'allowed_classes' would, using __PHP_Incomplete_Class.

The callback will be triggered after allowed_classes is evaluated (if present).

This callback would solve a few problems where allowed_classes is not sufficient:

  • It would allow for an is_subclass_of() check where for example an interface can be added to classes that are safe to get unserialized.

  • It would allow for better ways to fail, I think the __PHP_Incomplete_Class situation is kinda yucky. Since this is a callable the developer could just find another solution, for example throwing an exception, do a trigger_error() or exit().

  • This would also allow for fixing legacy applications where it is not exactly clear what is being unserialized. The callable would return a true value but a E_USER_DEPRECATION is triggered. This way data can be collected about what classes to allow. Providing a non-disrupting way to secure unserialize calls. This is especially helpful in very generic unserialize usages like caches.

  • This would make the unserialize_callback_func ini setting redundant for many use-cases, as any custom autoloading routine could also be executed in this allowed_classes_callback.

Feedback is very much appreciated.

@Girgias
Copy link
Member

Girgias commented Jul 10, 2025

I'm not exactly sure of all the interactions, but can't this be solved by using the unserialize_callback_func INI setting?

@langemeijer
Copy link
Author

That ini setting works like an autoloader. It probably pre-dates autoloading, but I didn't check.

From the manual:

The callback specified is called when unserialize() attempts to use an undefined class.

@langemeijer
Copy link
Author

langemeijer commented Jul 10, 2025

I checked: Yes indeed. Young Derick merged unserialize_callback_func ending November 2001, probably hitting 4.3. There is no mention of it in any changelog, but I guess it's too late to complain. :-) This pre-dates autoloading (introduced in 5.0) by a few years. I love git-archeology.

Back then this callback was very convenient, because it was assumed to be very reasonable that before unserializing, you never knew what your serialized string might hold. And this was therefore one of the first places almost everybody needed some autoloading. In the mean time we have learnt that it's pretty dangerous that we don't know what is being unserialized, and we want to limit this as much as we can.

This article by Mathieu Farrell is an excelect writeup btw:
https://blog.quarkslab.com/php-deserialization-attacks-and-a-new-gadget-chain-in-laravel.html

Problem is that for older applications we sometimes still don't know what is being unserialized and there is no easy way to find out reliably because it is in the data, not the code. My PR is aimed to solve this problem and help close this security issue.

@langemeijer
Copy link
Author

langemeijer commented Jul 14, 2025

I think for a realistic refactor-path allowed_classes_callback is needed for larger projects. I needed it for mine. Also I think that allowed_classes_callback offers a better developer experience than the allowed_classes array.

In my opinion the old PHP4 unserialize_callback_func ini setting should be deprecated as soon as possible, because it is confusing and adds nothing that cannot be solved already with standard autoloading.

My long-term agenda is pushing to close the security gap in unserialize():

I wish in the future (hopefully 8.6/9.0?) we could throw a deprecation message when an object is unserialize()'d without specifying allowed_classes or allowed_classes_callback, when a class was disallowed, and when __PHP_Incomplete_Class would used.

After deprecation, unserialize() should never return anything objects other than explicitly allowed classes. Any disallowed or unknown classes should trigger an exception. One could always do:

'allowed_classes_callback' => fn ($className) => true,

but that is obviously dangerous.

@TimWolla
Copy link
Member

Related: https://wiki.php.net/rfc/improve_unserialize_error_handling (see Future Scope).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants