Skip to content

Security: add allowed_classes to all unserialize() call sites to prevent PHP Object Injection - #12023

Open
XananasX7 wants to merge 1 commit into
WordPress:trunkfrom
XananasX7:security/unserialize-allowed-classes
Open

Security: add allowed_classes to all unserialize() call sites to prevent PHP Object Injection#12023
XananasX7 wants to merge 1 commit into
WordPress:trunkfrom
XananasX7:security/unserialize-allowed-classes

Conversation

@XananasX7

@XananasX7 XananasX7 commented May 31, 2026

Copy link
Copy Markdown

Trac ticket: https://core.trac.wordpress.org/ticket/62811

Summary

Multiple unserialize() call sites in WordPress core pass no allowed_classes option, allowing PHP gadget-chain attacks (PHP Object Injection / POI) if an attacker can influence the serialized data. This PR adds allowed_classes => false to 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.phpmaybe_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. Passing allowed_classes => false prevents any gadget-chain exploitation without any functional change.

Widget unserialize paths (4 files)

  • src/wp-includes/blocks/legacy-widget.php
  • src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php
  • src/wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php
  • src/wp-includes/class-wp-customize-widgets.php

All 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 => false provides a second layer. Widget instances are always plain array values.

SimplePie cache backends (5 files)

  • src/wp-includes/SimplePie/src/Cache/Redis.php
  • src/wp-includes/SimplePie/src/Cache/Memcache.php
  • src/wp-includes/SimplePie/src/Cache/Memcached.php
  • src/wp-includes/SimplePie/src/Cache/File.php
  • src/wp-includes/SimplePie/src/Cache/MySQL.php

SimplePie 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 => false closes 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 => false causes unserialize() to return an __PHP_Incomplete_Class stub 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

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.
@github-actions

Copy link
Copy Markdown

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 props-bot label.

Unlinked Accounts

The 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.

@XananasX7

Copy link
Copy Markdown
Author

Trac ticket submitted at https://core.trac.wordpress.org/ticket/62811 (PHP Object Injection hardening — add allowed_classes to maybe_unserialize() and widget/SimplePie unserialize call sites).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant