We were recently contacted by Wordfence and Patchstack regarding PHP Object Injection vulnerabilities related to the use of unserialize() in Better Search Replace and WP Migrate, respectively. An additional internal review of all Delicious Brains plugins led to the security releases of five plugins and two open-source libraries. We recommend immediately updating to the following patched versions.
Patched plugins and libraries
- Better Search Replace and Better Search Replace Pro 1.4.5
- WP Migrate Lite 2.6.10 and WP Migrate Pro 2.6.11
- WP Offload Media Lite and WP Offload Media Pro 3.2.6
- WP Offload SES Lite and WP Offload SES Pro 1.6.7
- WP Engine Site Migration 1.0.0-beta.24
- WP Background Processing 1.3.0
- WP Queue 2.1.0
In all of the above plugins and libraries, it is important to note that the PHP Object Injection vulnerability could only be exploited if it existed alongside an existing Property Oriented Programming (POP) chain found in a theme, plugin, or specific version of WordPress core. Neither Wordfence nor Patchstack found POP chains to be present in Better Search Replace or WP Migrate, respectively. Furthermore, the vulnerabilities related to find and replace operations could only be exploited if an authenticated user initiated the find and replace operation.
The rest of this article provides context around the vulnerabilities and our response. We will also share guidelines we’ve established regarding the use of unserialize()
that may be beneficial to the wider WordPress community.
Why we use unserialize()
If you spend enough time in WordPress databases, you will inevitably encounter serialized data stored by WordPress core, themes, and plugins. The widespread use of serialization means that unserialize()
is sometimes necessary to transform that data from a format that is optimized for storage to one that is more suitable for manipulation. In our plugins, deserialization is required to make processes like find and replace operations and URL rewriting possible.
Understanding the vulnerability
Both of the reported vulnerabilities from Wordfence and Patchstack concern the use of unserialize()
with untrusted objects that were either stored in the database or provided as user input.
Perhaps the best way to understand the vulnerability is to walk through a find and replace operation, including the specialized handling of serialized data that is necessary to prevent data corruption.
Let’s say you want to find all instances of an old domain and replace it with a new domain. To do so, our plugins loop over each row in the database in search of a match for the old domain. If a serialized object is encountered, then the following process unfolds:
- The object is deserialized using the
unserialize()
function. - As a result of passing the object to
unserialize()
with no additional arguments, the complete object is instantiated, potentially running magic methods defined in the corresponding PHP class. - If a match is found, then the old domain is replaced with the new domain.
- Finally, the object is re-serialized using the
serialize()
function and inserted back into the database.
The vulnerability can be seen in step 2, where the instantiation of the complete object and running of magic methods could allow a malicious object to execute code if a POP chain is present.
Addressing the vulnerability
Patching the PHP Object Injection vulnerability required careful consideration of the context surrounding each use of unserialize()
throughout our plugins. At the end of our internal review, we settled on three guidelines.
Guidelines for using unserialize()
in WordPress
These three guidelines address the different contexts where deserialization may occur within a WordPress plugin.
1. Prefer JSON encoding over serialization for untrusted input
First and foremost, the advice provided by Wordfence and Patchstack is to use JSON encoding instead of serialization whenever untrusted input is involved. In cases such as plugin configuration via settings pages, we are now defaulting to JSON encoding when handling user input.
The PHP Manual further reinforces this guidance:
Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this. Use a safe, standard data interchange format such as JSON (via
json_decode()
andjson_encode()
) if you need to pass serialized data to the user.
2. Pass 'allowed_classes' => false
during find and replace
The nature of a find and replace operation requires our plugins to manipulate database content that we had no hand in storing, which means JSON encoding is not a suitable solution in this context. However there are still steps we can take to prevent malicious objects from executing code.
During a find and replace, we’re only concerned with manipulating strings of various lengths while avoiding data corruption. Therefore we don’t actually need to instantiate a complete object as a result of deserialization. When running unserialize()
, we now set allowed_classes
to false
, which causes the object to be instantiated as __PHP_Incomplete_Class
and prevents those potentially dangerous magic methods from running.
$unserialized_string = @unserialize( $serialized_string, array('allowed_classes' => false ) );
When necessary, we have also included a polyfill to maintain backwards compatibility regarding the use of allowed_classes
in PHP 5.6.
3. Pass a list of 'allowed_classes'
when deserialization requires a complete object
In the open-source WP Background Processing and WP Queue libraries, unserialize()
is used to deserialize objects of specific classes defined by the libraries themselves. In these cases, we now enable passing an explicit list of allowed_classes
to reduce the risk of a malicious object being completely instantiated by the library.
For example, when creating a WP Queue database connection, an array of allowed Job subclasses may be provided.
$this->connection = new DatabaseConnection( $wpdb, array( Email_Job::class ) );
Refer to the README of WP Background Processing and WP Queue for more information and examples.
Conclusion
By addressing these vulnerabilities, we continue our commitment to providing secure solutions to our customers and the many users of our free plugins and open-source libraries. We also hope that the guidelines established around the use of unserialize()
will help others to improve the security of their WordPress products.
Many thanks to Sam Pizzey (Wordfence) and Dave Jong (Patchstack) for responsible disclosure, to the WP Engine Security team for their guidance, and to our engineers for their dedication in thoroughly addressing the issue.