---
title: "WordPress Unserialize Vulnerability: 3 Fixes, 1 Pattern"
slug: "wordpress-unserialize-vulnerability-three-fixes"
description: "A WordPress unserialize vulnerability tagged unauthenticated can still need an admin click. We tested three plugin fixes in a lab to see what each one needs."
date: 2026-10-11
author: "WP Vanguard Team"
category: "Research"
tags: ["wordpress-unserialize-vulnerability", "mainwp-child", "string-locator", "allowed-classes", "deserialization"]
featured_image: "/images/blog/wordpress-unserialize-vulnerability-three-fixes.jpg"
---

Three WordPress plugins fixed an unserialize vulnerability in the last 30 days: Booking Activities on September 11, String Locator on September 29 and MainWP Child on October 5. All three advisories read "unauthenticated PHP object injection" and all three score 8.1. We installed the vulnerable and fixed versions side by side in a lab and watched what happened.

```stat
{"value": "3 of 3", "label": "old versions created an object from planted data; none of the fixed versions did", "note": "Tested in a throwaway WordPress 7.1.2 container on PHP 8.3 with a harmless marker class."}
```

The sink is real in every case. What the label leaves out is who has to press the button. For two of the three, the unserialize call sits behind an administrator-only action, and the attacker's whole job is to leave bytes lying around for an admin to process later. That changes how urgent the update is, though it doesn't change whether you should do it.

## What We Tested

For each plugin we installed the last vulnerable version and the first fixed version in a clean container. Then we defined a harmless marker class that writes a line to a log when PHP wakes it up, serialized one instance, and fed it to the code path the patch changes. If the log gets a line, the plugin instantiated an object from data it shouldn't have trusted.

That proves the sink. It doesn't prove a takeover. Turning an object into code execution needs a gadget chain, a class already on the site whose magic methods do something dangerous when woken up. We didn't build one, we didn't look for one, and we're not publishing payloads. All three score 8.1 rather than 9.8, which fits an attack that needs conditions beyond the bug itself.

We also didn't test the first step, how an attacker gets serialized bytes into the site. Each advisory describes that step and we've taken it as written.

## The Three Fixes Side by Side

| Plugin | Installs | Where the unserialize runs | Who triggers it | Old version | Fixed version |
|---|---|---|---|---|---|
| String Locator | 100,000+ | Check that runs when a database row is saved in the SQL editor | An administrator | Object created (2.6.7) | No object (2.6.8) |
| MainWP Child | 700,000+ | Search and replace over database cells while cloning a backup | An administrator | Object created (6.2.1) | No object (6.2.2) |
| Booking Activities | 3,000+ | Reading stored booking metadata | Any code that reads the row | Object created (1.18.7.1) | No object (1.18.8) |

**String Locator (CVE-2026-97188).** The "validate serialized data" check runs when an administrator saves an edited row in the plugin's SQL editor. We confirmed the save route rejects anonymous requests with a 401. So the bytes have to be sitting in a database row first, and an administrator has to open that row and save it. Version 2.6.8 passes `allowed_classes => false`, so the check still tells you whether the data is well-formed but never builds the objects. Its predecessor's changelog already said "fixed secured unserialized data handling" in January 2025, which makes this the second pass at the same line.

**MainWP Child (CVE-2026-105436).** The code is in the clone feature. When a backup is extracted, the plugin runs a search and replace over every database cell, and it unserialized each value to do it. The extract action is an admin-ajax action with a capability check and a nonce, and an anonymous call to it returns `0`. The risk is a hostile or compromised clone source. Version 6.2.2 rewrote the routine: restricted decoding, a nesting limit of 32 levels, a cap of 100,000 values per cell, and a scan that rejects anything containing an object-like token. If a cell fails any of that, the plugin leaves it untouched. In our lab the original cell came back byte for byte.

**Booking Activities (CVE-2026-97248).** This is the one with no admin step. The metadata reader ran `maybe_unserialize()` on whatever it found in the plugin's table, so any code path that read a poisoned row created the object. Version 1.18.5 on August 3 had already stopped serialized values from being saved through text fields. Version 1.18.8 went further: it blocks object creation on read and deletes existing rows that contain a serialized object. After the update ran in our lab, zero such rows were left.

The Events Calendar patched the same family on September 10, which we covered in our [post on its widget-copy bugs](/blog/the-events-calendar-unauthenticated-rce-cve-2026-78159). That fix stops storing the submitted bytes and signs a cleaned copy instead.

## Reading the Unauthenticated Label

In the 14 days to October 11, our database logged 23 critical or high PHP object injection disclosures. Six carry the unauthenticated label. The label is accurate about the attacker: planting the data needs no account. It says nothing about the trigger.

Three questions sort these bugs better than the score does.

1. **Who plants the data?** A public form, a comment or a clone source can each carry bytes onto a site. This is where "unauthenticated" comes from.
2. **Who runs the unserialize?** An administrator clicking Save, a background job, or any visitor who loads a page. The answer decides how likely the trigger is.
3. **Is there a gadget chain?** Without a class that does something harmful on wake-up, an object is only an object.

```timeline
{"title": "Three fixes in 24 days", "data": [
 {"label": "Sep 11", "sub": "Booking Activities", "text": "1.18.8 blocks objects on read"},
 {"label": "Sep 29", "sub": "String Locator", "text": "2.6.8 restricts the SQL check"},
 {"label": "Oct 5", "sub": "MainWP Child", "text": "6.2.2 hardens the clone path"}
]}
```

> [!KEY]
> An unserialize bug needs three things at once: planted bytes, a trigger and a gadget chain. The label tells you about the first. Check the other two before you decide how fast to move.

We're not arguing against updating. Each fix shipped before its advisory went public, by 1 to 17 days, the changes are small, and a stored object that nobody triggers today can be triggered tomorrow by a new plugin that adds the missing gadget. The point is triage. MainWP Child on a site that never clones is a lower-urgency update than Booking Activities on a site that takes public bookings, even though MainWP has more than 200 times the installs.

## The Fix Is One Line

Every one of these patches comes down to passing a second argument to `unserialize()`:

```php
$data = unserialize( $value, [ 'allowed_classes' => false ] );
```

With that option, PHP turns any object into a placeholder class instead of creating it. If your code only needs to know whether the value is valid, or only needs arrays and strings, nothing else has to change.

Two details are worth knowing. MainWP's patch notes that enum values can still trigger an autoloader even with classes blocked, which is why its scanner rejects enum tokens as well. And re-serializing a placeholder object restores the original class name, so MainWP refuses to write such values back at all. If you maintain plugin code, store plain data as JSON where you can, and treat any `unserialize()` on data you didn't write as a bug until proven otherwise. Our earlier look at [PHP object injection in WordPress plugins](/blog/php-object-injection-wordpress-plugins) has more on how gadget chains work.

## What to Do on Your Sites

Check the three versions first:

```bash
wp plugin list --fields=name,status,version,update_version | grep -i -E "string-locator|mainwp-child|booking-activities"
```

You want String Locator 2.6.8 or later, MainWP Child 6.2.2 or later and Booking Activities 1.18.8 or later.

**Remove one-off admin tools when the job is done.** String Locator is a search tool. Many sites installed it for a single afternoon and never removed it. Deactivated plugins still sit on disk, so delete it.

**Update MainWP Child on every managed site.** The plugin lives on each client site, not on your dashboard. A fleet of 40 sites means 40 updates, and the dashboard's update screen is the quickest way to do them.

**Look for serialized objects you didn't expect.** This lists candidates for review across the common tables:

```bash
wp db query "SELECT 'postmeta' AS src, COUNT(*) AS n FROM wp_postmeta WHERE meta_value LIKE 'O:%' UNION ALL SELECT 'usermeta', COUNT(*) FROM wp_usermeta WHERE meta_value LIKE 'O:%' UNION ALL SELECT 'commentmeta', COUNT(*) FROM wp_commentmeta WHERE meta_value LIKE 'O:%' UNION ALL SELECT 'options', COUNT(*) FROM wp_options WHERE option_value LIKE 'O:%'"
```

A clean WordPress install returns three rows in options. They're core's own update transients. Anything else deserves a look at the class name, because plenty of legitimate plugins store objects too. Treat the output as a review list, not a verdict. Your table prefix may differ from `wp_`.

## If You Were Behind When the Fix Shipped

Updating stops the next unserialize. It doesn't remove data a visitor planted earlier. Look through the rows above for class names you don't recognize, and check the administrator list for accounts you didn't create. Our guide to the [signs your WordPress site has been hacked](/blog/signs-wordpress-site-hacked) covers the rest, and the [malware removal guide](/blog/wordpress-malware-removal-guide) walks through cleanup.

We track disclosures like these every day and list the ones worth patching first on our [weekly patch list](/blog/wordpress-plugins-to-patch-this-week). To check what's installed on your own site against the same database, [run a scan](/#scanner).

## References

- [Wordfence Intelligence vulnerability database](https://www.wordfence.com/threat-intel/vulnerabilities/)
- [PHP manual: unserialize()](https://www.php.net/manual/en/function.unserialize.php)
- [String Locator changelog on WordPress.org](https://wordpress.org/plugins/string-locator/#developers)
- [MainWP Child changelog on WordPress.org](https://wordpress.org/plugins/mainwp-child/#developers)
- [Booking Activities changelog on WordPress.org](https://wordpress.org/plugins/booking-activities/#developers)
- CVE-2026-97188 (String Locator), CVE-2026-105436 (MainWP Child), CVE-2026-97248 (Booking Activities)
