Security: add allowed_classes to all unserialize() call sites to prevent PHP Object Injection - #12023
Conversation
Harden multiple unserialize() call sites against PHP Object Injection (POI) by restricting or eliminating allowed object classes. PHP 7.0+ supports the allowed_classes option; passing false prevents instantiation of arbitrary classes, which blocks the first step of any gadget-chain attack. maybe_unserialize() (functions.php): Used to decode option values, user meta, and widget settings from the database. Since all callers expect plain scalars/arrays, passing allowed_classes => false is safe and covers hundreds of indirect call sites. Widget unserialize (legacy-widget.php, class-wp-rest-widgets-controller.php, class-wp-rest-widget-types-controller.php, class-wp-customize-widgets.php): These already validate the data with wp_hash() / HMAC before deserializing, but defense-in-depth requires allowed_classes => false. Widget instances are always plain arrays of configuration scalars. SimplePie cache backends (Redis, Memcache, Memcached, File, MySQL): Cache stores are external surfaces (Redis, Memcache servers, filesystem). SimplePie only caches feed data as nested arrays of strings/ints — no PHP objects. allowed_classes => false is correct and safe for all five backends.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @XananasX7. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Trac ticket submitted at https://core.trac.wordpress.org/ticket/62811 (PHP Object Injection hardening — add |
Trac ticket: https://core.trac.wordpress.org/ticket/62811
Summary
Multiple
unserialize()call sites in WordPress core pass noallowed_classesoption, allowing PHP gadget-chain attacks (PHP Object Injection / POI) if an attacker can influence the serialized data. This PR addsallowed_classes => falseto every call site where only scalar/array data is expected, and defense-in-depth to all HMAC-validated widget deserialization paths.Files Changed
src/wp-includes/functions.php—maybe_unserialize()This is the most impactful change.
maybe_unserialize()is called in hundreds of places throughout WordPress core to decode option values (wp_options), user meta, post meta, and widget settings from the database. The stored values are always scalars, arrays, or simple objects — never complex class instances with destructors or magic methods. Passingallowed_classes => falseprevents any gadget-chain exploitation without any functional change.Widget unserialize paths (4 files)
src/wp-includes/blocks/legacy-widget.phpsrc/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.phpsrc/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.phpsrc/wp-includes/class-wp-customize-widgets.phpAll four already validate the serialized data via
wp_hash()/ HMAC before deserializing. Defense-in-depth still applies: if a server-side key compromise or hash collision were exploited,allowed_classes => falseprovides a second layer. Widget instances are always plainarrayvalues.SimplePie cache backends (5 files)
src/wp-includes/SimplePie/src/Cache/Redis.phpsrc/wp-includes/SimplePie/src/Cache/Memcache.phpsrc/wp-includes/SimplePie/src/Cache/Memcached.phpsrc/wp-includes/SimplePie/src/Cache/File.phpsrc/wp-includes/SimplePie/src/Cache/MySQL.phpSimplePie stores RSS/Atom feed data as nested arrays of strings and integers — no PHP class instances. An attacker with write access to the Redis/Memcache/filesystem cache could inject a serialized gadget chain.
allowed_classes => falsecloses this attack path on all five backends.Risk Without This Fix
PHP Object Injection via
unserialize()is a well-documented RCE vector. WordPress's autoloader makes thousands of classes available at deserialization time, including many with__destruct()and__wakeup()methods that can be chained into arbitrary code execution (similar to exploits against Magento, Drupal, and Joomla in prior CVEs).Compatibility
allowed_classes => falsecausesunserialize()to return an__PHP_Incomplete_Classstub instead of instantiating unexpected classes. Since no call site in this PR is expected to produce PHP objects, there is no functional change for any legitimate WordPress installation or plugin. The only breakage would be a plugin that stores serialized class instances in a widget setting — an undocumented and unsupported pattern.References
unserialize()allowed_classesoption: https://www.php.net/manual/en/function.unserialize.php