Atomic Edge analysis of CVE-2026-57722:
This vulnerability affects the Enable Media Replace plugin for WordPress, versions up to and including 4.2.1. It is a Stored Cross-Site Scripting (XSS) flaw that allows authenticated attackers with editor-level access or higher to inject arbitrary web scripts. The vulnerability only affects multi-site installations or those where unfiltered_html has been disabled. The CVSS score is 4.4, indicating a moderate severity issue.
Root Cause: The root cause lies in insufficient input sanitization and output escaping in three specific locations within the plugin. In `classes/emr-plugin.php`, lines 608-620, the `$modified` and `$display_name` variables are output without escaping via `esc_html()`. The `$modified` value comes from `date_i18n(__(‘M j, Y @ H:i’), strtotime($post->post_modified))` and `$display_name` from `get_the_author_meta(‘display_name’, $author_id)`. These values are rendered directly into the HTML output without sanitization, enabling stored XSS when an attacker can control the `post_modified` timestamp or author display name through other means.
Exploitation: An attacker with editor-level access can exploit this by modifying a post’s modified timestamp or author display name to include malicious JavaScript. For example, through the WordPress REST API or direct database manipulation, the attacker could set `post_modified` to a value containing `alert(‘XSS’)`. When an administrator or other user views the media library edit screen, the injected script executes. The attacker must have editor-level access to trigger the vulnerable output, but the script runs in the context of any user viewing the injected page.
Patch Analysis: The patch adds `esc_html()` calls around the `$modified` and `$display_name` variables at lines 608 and 619 of `emr-plugin.php`. Before the patch, these variables were output raw. After the patch, they are properly HTML-escaped, preventing the execution of any injected HTML or JavaScript. The patch also includes minor code reorganization in other files (DirectoryModel.php, ShortPixelLogger.php, Replacer.php) and a version bump to 4.2.2.
Impact: Successful exploitation allows an authenticated attacker with editor-level access to perform stored cross-site scripting attacks. This can lead to session hijacking, defacement, redirection to malicious sites, or theft of sensitive data displayed on the affected pages. The attacker can execute arbitrary JavaScript in the context of any user visiting the compromised media library edit screen.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/enable-media-replace/build/shortpixel/filesystem/src/Model/File/DirectoryModel.php
+++ b/enable-media-replace/build/shortpixel/filesystem/src/Model/File/DirectoryModel.php
@@ -350,6 +350,11 @@
public function getPermissionRecursive()
{
$parent = $this->getParent();
+ // Edge-case when the whole structure doesn't exist and / or getParent can only retrieve same-level path ?
+ if (false === $parent)
+ {
+ return false;
+ }
if (! $parent->exists())
{
return $parent->getPermissionRecursive();
--- a/enable-media-replace/build/shortpixel/log/src/ShortPixelLogger.php
+++ b/enable-media-replace/build/shortpixel/log/src/ShortPixelLogger.php
@@ -54,10 +54,8 @@
$this->namespace = substr($ns, 0, strpos($ns, '\')); // try to get first part of namespace
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is not a form
- if (isset($_REQUEST['SHORTPIXEL_DEBUG']))
+ if (isset($_REQUEST['SHORTPIXEL_DEBUG']) && true === $this->checkUserLevel()) // manual takes precedence over constants
{
-
- // Note! User access level is checked in Addlog and Loadview to prevent lower than administrator access. It can't be checked early, because the user functions might not be loaded before first logs
$this->is_manual_request = true;
$this->is_active = true;
--- a/enable-media-replace/build/shortpixel/replacer/src/Replacer.php
+++ b/enable-media-replace/build/shortpixel/replacer/src/Replacer.php
@@ -318,12 +318,10 @@
$sql = $wpdb->prepare($sql, '%' . $url . '%');
- Log::addTemp('Checking -- ', $sql);
// This is a desparate solution. Can't find anyway for wpdb->prepare not the add extra slashes to the query, which messes up the query.
$rsmeta = $wpdb->get_results($sql, ARRAY_A);
- Log::addTemp('result -- ' . count($rsmeta) , $rsmeta);
if (! empty($rsmeta))
{
@@ -474,7 +472,6 @@
$in_deep === false && (is_array($content) || is_object($content))
)
{
- Log::addTemp('Content is array or object - not json, - maybe serializing');
$content = maybe_serialize($content);
}
return $content;
@@ -582,18 +579,15 @@
if (! isset($this->source_metadata['sizes'][$sizeName]) || ! isset($this->target_metadata['width'])) // This can happen with non-image files like PDF.
{
-
+ // Check if metadata-less item is a svg file. Just the main file to replace all thumbnails since SVG's don't need thumbnails.
+ if (strpos($this->target_url, '.svg') !== false)
+ {
+ $svg_file = wp_basename($this->target_url);
+ return $svg_file; // this is the relpath of the mainfile.
+ }
return false;
}
-
- // Check if metadata-less item is a svg file. Just the main file to replace all thumbnails since SVG's don't need thumbnails.
- if (strpos($this->target_url, '.svg') !== false)
- {
- $svg_file = wp_basename($this->target_url);
- return $svg_file; // this is the relpath of the mainfile.
- }
-
$old_width = $this->source_metadata['sizes'][$sizeName]['width']; // the width from size not in new image
$new_width = $this->target_metadata['width']; // default check - the width of the main image
--- a/enable-media-replace/classes/emr-plugin.php
+++ b/enable-media-replace/classes/emr-plugin.php
@@ -608,7 +608,7 @@
$modified = date_i18n(__('M j, Y @ H:i'), strtotime($post->post_modified));
?>
<div class="misc-pub-section curtime">
- <span id="timestamp"><?php echo esc_html__('Revised', 'enable-media-replace'); ?>: <b><?php echo $modified; ?></b></span>
+ <span id="timestamp"><?php echo esc_html__('Revised', 'enable-media-replace'); ?>: <b><?php echo esc_html($modified); ?></b></span>
</div>
<?php
@@ -619,7 +619,7 @@
$display_name = get_the_author_meta('display_name', $author_id);
?>
<div class="misc-pub-section replace_author">
- <span><?php echo esc_html__('Replaced By', 'enable-media-replace'); ?>: <b><?php echo $display_name; ?></b></span>
+ <span><?php echo esc_html__('Replaced By', 'enable-media-replace'); ?>: <b><?php echo esc_html($display_name); ?></b></span>
</div>
<?php
}
--- a/enable-media-replace/enable-media-replace.php
+++ b/enable-media-replace/enable-media-replace.php
@@ -3,7 +3,7 @@
* Plugin Name: Enable Media Replace
* Plugin URI: https://shortpixel.com
* Description: Enable replacing media files by uploading a new file in the "Edit Media" section of the WordPress Media Library.
- * Version: 4.2.1
+ * Version: 4.2.2
* Author: ShortPixel
* Author URI: https://shortpixel.com
* GitHub Plugin URI: https://github.com/short-pixel-optimizer/enable-media-replace
@@ -25,7 +25,7 @@
*
*/
-define( 'EMR_VERSION', '4.2.1' );
+define( 'EMR_VERSION', '4.2.2' );
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.