Research · 7 min read

ShortPixel Vulnerability: A Safety Flag That Didn't Recurse

By WP Vanguard Team

ShortPixel Vulnerability: A Safety Flag That Didn't Recurse

ShortPixel Image Optimizer runs on about 300,000 WordPress sites. On September 17, 2026, Wordfence disclosed CVE-2026-17086 against every version up to and including 6.5.5, scored 8.8. An author-level account can inject a PHP object into the site.

What makes this one worth reading isn't the score. It's that the developer wrote the correct defense, called it correctly from the risky code path, and still shipped the bug. The guard was there. It just didn't survive the trip down one level of nesting.

The guard existed and was called correctly

The vulnerable code lives in the plugin's bundled URL replacer, build/shortpixel/replacer2/src/Replacer.php. When ShortPixel rewrites image URLs after an optimization, it walks post content and metadata looking for old URLs to swap.

Some of that content is serialized PHP, so the replacer has to unserialize it, rewrite, and reserialize. That's the dangerous part, and the author knew it. Here's the signature in 6.5.5, at line 385:

public function replaceContent($content, $search, $replace, $in_deep = false, $strict_check = false)

And the docblock right above it spells out the intent:

@param $strict_check Boolean . If true, remove all classes from serialization
check and fail. This should be done on post_content, not on metadata.

The branch that flag controls sits at line 397:

if (true === $strict_check) {
    $args = array('allowed_classes' => false);
} else {
    $args = array('allowed_classes' => true);
}

$content = Unserialize::unserialize($content, $args);

allowed_classes => false is PHP's built-in answer to object injection. It tells unserialize() to refuse to instantiate any class at all, turning every object in the payload into a harmless __PHP_Incomplete_Class. No constructor, no destructor, no gadget chain.

And the plugin does ask for it on the risky path. Line 259, in the query that rewrites post content:

$replaced_content = $this->replaceContent($post_content, $search_urls, $replace_urls, false, true);

That fifth argument is $strict_check, and it's true. Post content gets strict handling, exactly as the docblock says it should.

The flag doesn't survive the recursion

replaceContent() is recursive. Once it unserializes a structure, it walks into arrays and objects and calls itself on each value. Those three recursive calls, at lines 425, 429 and 446 in 6.5.5, look like this:

$content[$index] = $this->replaceContent($value, $search, $replace, true);

Count the arguments. There are four. $strict_check isn't passed, so it falls back to its default, which is false, which means allowed_classes => true.

So strict mode applies to the outermost layer and evaporates on the way in. The caller asks for safety, the first unserialize() honors it, and every level below runs with class instantiation fully enabled. That's the entire vulnerability in one missing argument.

This is why Wordfence's title calls out nested content. The nesting isn't incidental. It's the whole mechanism. A payload sitting at the top level gets refused; the same payload one level deep sails through.

What an attacker actually needs

The entry point is doReplaceQuery(), which pulls candidate posts straight from the database:

SELECT ID, post_content FROM wp_posts
WHERE post_status in ('publish','future','draft','pending','private')
  AND post_content LIKE %s

Note draft and pending in that list. An author doesn't need to publish anything. A saved draft containing the site's upload base URL is enough to get the post selected, and authors can create drafts by definition. That's the whole reason this scores PR:L rather than PR:H on the CVSS vector of AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H.

From there the attacker stores a serialized structure whose inner element is itself a serialized object, waits for a URL replacement to run, and the nested layer gets unserialized with classes allowed.

The 8.8 depends on plugins ShortPixel doesn't ship

Here's the part most coverage will skip, and it changes what you should do about this.

Wordfence's own advisory says no known POP chain exists in ShortPixel itself. A POP chain is the gadget you need to turn an injected object into an actual consequence: a class with a __destruct() or __wakeup() that touches the filesystem, or runs a callback, or leaks data. Object injection without a chain instantiates an object that then does nothing interesting.

So on a site running ShortPixel and nothing else with a usable gadget, this bug is close to inert. On a site where some other plugin or theme ships a chain, the same bug becomes arbitrary file deletion, data disclosure or remote code execution, depending on what that chain reaches.

That's an uncomfortable property. The severity of a flaw in one plugin gets decided by your other plugins. It's the inverse of the GiveWP vulnerability we covered, where the plugin bundled the gadget chain that weaponized its own injection point and needed no help from anything else on the site. Treat the 8.8 as the ceiling, not the forecast, and don't let "no known chain" talk you out of updating. Your plugin list decides where in that range you sit, and it changes every time you install something.

How ShortPixel fixed it

Version 6.5.6 does three things, and the third is the one that matters:

// 1. The default is now safe
public function replaceContent($content, $search, $replace, $in_deep = false, $strict_check = true)

// 2. The flag is passed down through every recursive call
$content[$index] = $this->replaceContent($value, $search, $replace, true, $strict_check);

// 3. The branch is gone entirely
$args = array('allowed_classes' => false);

Flipping the default and threading the flag through recursion both fix the reported bug. Deleting the branch is what stops the next one, because now no caller anywhere can ask for object instantiation, whether deliberately or by forgetting an argument. The safest flag is the one that isn't a flag.

That's the reusable lesson. A security control that travels as an optional function parameter will eventually get dropped at a call site, and recursive functions are where it gets dropped quietest, because the call that loses it is written by the same person who wrote the guard. We saw a similar shape in how esc_sql corrupted serialized data in Tutor LMS: the protective function was present and doing its job, and the damage came from where it sat relative to everything else.

What to do now

Update ShortPixel Image Optimizer to 6.5.6 or later. That's the fix, and it's the whole fix.

After updating, take a moment on the accounts that made this reachable. Author is not a throwaway role here. Check your user list for author-and-above accounts you don't recognize or no longer need, since this class of bug turns a low-trust contributor account into a foothold whose reach depends on what else you have installed. If anything looks wrong, work through the signs your WordPress site has been hacked.

ShortPixel is one of several plugins worth patching right now. The rest of this week's list, including a WordPress core XSS and two closed plugins with no fix at all, is in our weekly patch list.

Run a Scan

Run a scan on your site to check your installed plugin versions against our vulnerability database.

References

shortpixel-vulnerability cve-2026-17086 php-object-injection allowed-classes wordpress-deserialization

Related reading

Check Your WordPress Site Security

Free scan, no login required. Find vulnerabilities before attackers do.

Scan Your Site Free

Get weekly WordPress security tips

Vulnerability alerts, plugin updates, and security guides. No spam. Unsubscribe any time.

WP Vanguard is built by Wbcom Designs, makers of Reign, Jetonomy, Listora, and more. Explore our WordPress products →
← Back to Blog