[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fL1Gmij21OJuLEHhtcQy8IyTDjGjtHiy29o1WALwsdsQ":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":28,"research_verified":29,"research_rounds_completed":30,"research_plan":31,"research_summary":32,"research_vulnerable_code":33,"research_fix_diff":34,"research_exploit_outline":35,"research_model_used":36,"research_started_at":37,"research_completed_at":38,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":29,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":29,"source_links":39},"CVE-2026-57722","enable-media-replace-authenticated-editor-stored-cross-site-scripting","Enable Media Replace \u003C= 4.2.1 - Authenticated (Editor+) Stored Cross-Site Scripting","The Enable Media Replace plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 4.2.1 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with editor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.","enable-media-replace",null,"\u003C=4.2.1","4.2.2","medium",4.4,"CVSS:3.1\u002FAV:N\u002FAC:H\u002FPR:H\u002FUI:N\u002FS:C\u002FC:L\u002FI:L\u002FA:N","Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')","2026-07-01 00:00:00","2026-07-07 19:58:17",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002Fc8fe4c70-c196-4f20-b787-bea223726fab?source=api-prod",7,[22,23,24,25,26,27],"build\u002Fshortpixel\u002Ffilesystem\u002Fsrc\u002FModel\u002FFile\u002FDirectoryModel.php","build\u002Fshortpixel\u002Flog\u002Fsrc\u002FShortPixelLogger.php","build\u002Fshortpixel\u002Freplacer\u002Fsrc\u002FReplacer.php","classes\u002Femr-plugin.php","enable-media-replace.php","readme.txt","researched",false,3,"This research plan focuses on **CVE-2026-57722**, a Stored Cross-Site Scripting (XSS) vulnerability in the **Enable Media Replace** plugin. \n\nThe vulnerability exists because the plugin's \"search and replace\" functionality—designed to update all links to a media file across the site—performs direct database updates on `post_content` without applying WordPress's KSES filters. This allows users with Editor-level permissions to bypass `unfiltered_html` restrictions (common in Multi-site setups) and inject malicious scripts into the site's content.\n\n---\n\n### 1. Vulnerability Summary\n*   **Vulnerability:** Authenticated Stored XSS\n*   **Location:** `build\u002Fshortpixel\u002Freplacer\u002Fsrc\u002FReplacer.php` and its database update logic.\n*   **Sink:** The `doReplaceQuery` method in `Replacer.php` which updates `wp_posts.post_content` and other database fields via direct SQL queries.\n*   **Cause:** Insufficient sanitization of the replacement string (derived from the new filename) and lack of output escaping when the modified content is rendered on the frontend.\n*   **Constraint:** Requires `unfiltered_html` to be disabled (e.g., Multi-site or `define( 'DISALLOW_UNFILTERED_HTML', true );`).\n\n### 2. Attack Vector Analysis\n*   **Endpoint:** `\u002Fwp-admin\u002Fupload.php?page=enable-media-replace\u002Fenable-media-replace.php`\n*   **Action:** Media Replacement (specifically Option 2: \"Replace the file, use the new file name, and update all links\").\n*   **Payload Location:** The filename of the uploaded replacement file.\n*   **Authentication:** Editor or Administrator role (specifically requires `upload_files` capability, as seen in `classes\u002Femr-plugin.php` line 52).\n*   **Precondition:** The site must have at least one existing post containing the media file being replaced.\n\n### 3. Code Flow\n1.  **Entry Point:** An Editor accesses the replacement screen for a specific attachment via `upload.php?page=enable-media-replace\u002Fenable-media-replace.php&attachment_id={ID}`.\n2.  **Processing:** The `EnableMediaReplace\\EnableMediaReplacePlugin::route()` method (inferred handler for the menu page) processes the form submission.\n3.  **Replacer Initialization:** The `EnableMediaReplace\\Replacer\\Replacer` class is instantiated.\n4.  **Metadata Generation:** `Replacer::replace()` calls `getRelativeURLS()` to determine the search string (old filename\u002FURL) and the replacement string (new filename\u002FURL).\n5.  **Target URL Source:** The replacement string is derived from the `$this->target_url` set during the upload process (e.g., `wp-content\u002Fuploads\u002F2023\u002F10\u002Fmalicious\u003Cscript>.png`).\n6.  **Database Sink:** `Replacer::replace()` calls `$this->doReplaceQuery($base_url, $search_urls, $replace_urls)`.\n7.  **Injection:** `doReplaceQuery` performs a `REPLACE()` operation in SQL on `wp_posts.post_content`. Because it uses direct SQL via `$wpdb`, it bypasses the `wp_kses` sanitization that usually restricts Editors from injecting `\u003Cscript>` tags when `unfiltered_html` is disabled.\n\n### 4. Nonce Acquisition Strategy\nThe \"Replace Media\" screen is an admin page that requires a nonce for form submission.\n\n1.  **Identify Screen:** The replacement form is rendered on the admin page with the slug `enable-media-replace\u002Fenable-media-replace.php`.\n2.  **Page Creation:** No special shortcode is needed, but an attachment must exist.\n3.  **Manual Navigation:**\n    *   Navigate to `wp-admin\u002Fupload.php`.\n    *   Identify an attachment ID (e.g., `12`).\n    *   Navigate to `wp-admin\u002Fupload.php?page=enable-media-replace\u002Fenable-media-replace.php&attachment_id=12`.\n4.  **Extraction:** The nonce is typically located in a hidden input field or localized JS. In version 4.2.1, EMR enqueues scripts for the UI.\n    *   **JavaScript Variable:** Check for `window.emr_data` or similar.\n    *   **Action String:** The nonce action is likely `enable-media-replace`.\n    *   **Extraction Command:** `browser_eval(\"document.querySelector('input[name=\\\"_wpnonce\\\"]')?.value\")`\n\n### 5. Exploitation Strategy\n1.  **Preparation:**\n    *   Create a post as Administrator and insert an image (ID: `X`).\n    *   Create an Editor user.\n2.  **Payload Creation:**\n    *   Create a valid image file named: `xss_payload\">\u003Cscript>alert(origin)\u003C\u002Fscript>.png`.\n3.  **Execution:**\n    *   Log in as Editor.\n    *   Fetch the replacement page for Attachment `X`.\n    *   Extract the `_wpnonce` and any required hidden fields (like `attachment_id`).\n    *   Submit a `POST` request to `upload.php?page=enable-media-replace\u002Fenable-media-replace.php` using `multipart\u002Fform-data`.\n4.  **Request Parameters:**\n    *   `action`: `emr_upload` (inferred) or the primary POST action.\n    *   `attachment_id`: `X`\n    *   `replace_type`: `replace_and_search` (This is critical to trigger the URL update logic).\n    *   `user_file`: The file named `xss_payload\">\u003Cscript>alert(origin)\u003C\u002Fscript>.png`.\n    *   `_wpnonce`: The extracted nonce.\n5.  **Trigger:** View the post created in Step 1. The image URL in the HTML will now contain the broken attribute and the script tag.\n\n### 6. Test Data Setup\n*   **Environment:** WordPress Multi-site OR Single site with `define('DISALLOW_UNFILTERED_HTML', true);` in `wp-config.php`.\n*   **User:** Editor role.\n*   **Content:** A published post containing an image: `\u003Cimg src=\"...\u002Fold-image.png\" ...>`.\n*   **Target Image:** An existing attachment in the media library.\n\n### 7. Expected Results\n*   The `wp_posts` table will contain the payload in the `post_content` column for the post using that image.\n*   When visiting the post on the frontend, the browser will execute `alert(origin)`.\n*   The `_wp_attached_file` meta for the attachment will also likely contain the payload, potentially causing XSS in the Media Library list view.\n\n### 8. Verification Steps (WP-CLI)\n1.  **Check Post Content:**\n    ```bash\n    wp db query \"SELECT post_content FROM wp_posts WHERE post_content LIKE '%\u003Cscript>%'\"\n    ```\n2.  **Check Attachment Meta:**\n    ```bash\n    wp post meta list [ATTACHMENT_ID] --keys=_wp_attached_file\n    ```\n\n### 9. Alternative Approaches\n*   **Logger XSS:** If `SHORTPIXEL_DEBUG` is enabled via `?SHORTPIXEL_DEBUG=true`, the `ShortPixelLogger` will log the metadata of the replacement. If the logger view (accessible to admins) does not escape the log contents, XSS will fire in the admin dashboard when they view the debug logs.\n*   **Metadata XSS:** Inject the payload into the \"Custom Date\" fields if the replacement form allows setting custom EXIF or attachment metadata, as these are often displayed unescaped in the attachment editor.","The Enable Media Replace plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) in versions up to 4.2.1. This vulnerability allows authenticated attackers with Editor-level access or above to inject arbitrary web scripts into pages and the administrative dashboard, bypassing standard WordPress filters like KSES by performing direct database updates during the media replacement process.","\u002F\u002F classes\u002Femr-plugin.php line 608\n        \u003Cdiv class=\"misc-pub-section curtime\">\n            \u003Cspan id=\"timestamp\">\u003C?php echo esc_html__('Revised', 'enable-media-replace'); ?>: \u003Cb>\u003C?php echo $modified; ?>\u003C\u002Fb>\u003C\u002Fspan>\n        \u003C\u002Fdiv>\n\n            \u003C?php\n        }\n\n        if ($author_id = get_post_meta($post->ID, '_active_author_id', true)) {\n            $display_name = get_the_author_meta('display_name', $author_id);\n            ?>\n      \u003Cdiv class=\"misc-pub-section replace_author\">\n        \u003Cspan>\u003C?php echo esc_html__('Replaced By', 'enable-media-replace'); ?>: \u003Cb>\u003C?php echo $display_name; ?>\u003C\u002Fb>\u003C\u002Fspan>\n      \u003C\u002Fdiv>\n\n---\n\n\u002F\u002F build\u002Fshortpixel\u002Freplacer\u002Fsrc\u002FReplacer.php around line 172\n\t    Log::addDebug('Doing meta search and replace -', array($search_urls, $replace_urls) );\n\t    Log::addDebug('Searching with BaseuRL ' . $base_url);\n\n\t    do_action('emr\u002Freplacer\u002Freplace_urls', $search_urls, $replace_urls);\n\t    $updated = 0;\n\n\t    $updated += $this->doReplaceQuery($base_url, $search_urls, $replace_urls);","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.1\u002Fbuild\u002Fshortpixel\u002Ffilesystem\u002Fsrc\u002FModel\u002FFile\u002FDirectoryModel.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.2\u002Fbuild\u002Fshortpixel\u002Ffilesystem\u002Fsrc\u002FModel\u002FFile\u002FDirectoryModel.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.1\u002Fbuild\u002Fshortpixel\u002Ffilesystem\u002Fsrc\u002FModel\u002FFile\u002FDirectoryModel.php\t2026-04-22 16:33:10.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.2\u002Fbuild\u002Fshortpixel\u002Ffilesystem\u002Fsrc\u002FModel\u002FFile\u002FDirectoryModel.php\t2026-06-28 20:49:46.000000000 +0000\n@@ -350,6 +350,11 @@\n \tpublic function getPermissionRecursive()\n \t{\n \t\t $parent = $this->getParent();\n+      \u002F\u002F Edge-case when the whole structure doesn't exist and \u002F or getParent can only retrieve same-level path ? \n+      if (false === $parent)\n+      {\n+          return false; \n+      }\n \t\t  if (! $parent->exists())\n \t\t\t{\n \t\t\t\t return $parent->getPermissionRecursive();\ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.1\u002Fbuild\u002Fshortpixel\u002Flog\u002Fsrc\u002FShortPixelLogger.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.2\u002Fbuild\u002Fshortpixel\u002Flog\u002Fsrc\u002FShortPixelLogger.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.1\u002Fbuild\u002Fshortpixel\u002Flog\u002Fsrc\u002FShortPixelLogger.php\t2026-04-22 16:33:10.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.2\u002Fbuild\u002Fshortpixel\u002Flog\u002Fsrc\u002FShortPixelLogger.php\t2026-06-28 20:49:46.000000000 +0000\n@@ -54,10 +54,8 @@\n     $this->namespace = substr($ns, 0, strpos($ns, '\\\\')); \u002F\u002F try to get first part of namespace\n \n     \u002F\u002F phpcs:ignore WordPress.Security.NonceVerification.Recommended  -- This is not a form\n-    if (isset($_REQUEST['SHORTPIXEL_DEBUG'])) \n+    if (isset($_REQUEST['SHORTPIXEL_DEBUG']) && true === $this->checkUserLevel()) \u002F\u002F manual takes precedence over constants\n     {\n-\n-      \u002F\u002F 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\n       $this->is_manual_request = true;\n       $this->is_active = true;\n \ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.1\u002Fbuild\u002Fshortpixel\u002Freplacer\u002Fsrc\u002FReplacer.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.2\u002Fbuild\u002Fshortpixel\u002Freplacer\u002Fsrc\u002FReplacer.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.1\u002Fbuild\u002Fshortpixel\u002Freplacer\u002Fsrc\u002FReplacer.php\t2026-06-24 15:55:14.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.2\u002Fbuild\u002Fshortpixel\u002Freplacer\u002Fsrc\u002FReplacer.php\t2026-06-28 20:49:46.000000000 +0000\n@@ -318,12 +318,10 @@\n \n \t        $sql = $wpdb->prepare($sql, '%' . $url . '%');\n \n-\t\t\tLog::addTemp('Checking -- ', $sql);\n \n \t        \u002F\u002F 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.\n \t        $rsmeta = $wpdb->get_results($sql, ARRAY_A);\n \n-\t\t\tLog::addTemp('result -- ' . count($rsmeta) , $rsmeta);\n \n \t        if (! empty($rsmeta))\n \t        {\n@@ -474,7 +472,6 @@\n \t\t\t \t\t\t$in_deep === false && (is_array($content) || is_object($content))\n \t\t\t\t\t\t)\n \t\t\t{\n-\t\t\t\tLog::addTemp('Content is array or object - not json, - maybe serializing');\n \t\t\t\t$content = maybe_serialize($content);\n \t\t\t}\n \t    return $content;\n@@ -582,18 +579,15 @@\n \n \t\t\tif (! isset($this->source_metadata['sizes'][$sizeName]) || ! isset($this->target_metadata['width'])) \u002F\u002F This can happen with non-image files like PDF.\n \t\t\t{\n-\n+\t\t\t\t \u002F\u002F Check if metadata-less item is a svg file. Just the main file to replace all thumbnails since SVG's don't need thumbnails.\n+\t\t\t\t if (strpos($this->target_url, '.svg') !== false)\n+\t\t\t\t {\n+\t\t\t\t\t$svg_file = wp_basename($this->target_url);\n+\t\t\t\t\treturn $svg_file;  \u002F\u002F this is the relpath of the mainfile.\n+\t\t\t\t }\n \n \t\t\t\treturn false;\n \t\t\t}\n-\n-\t\t\t\u002F\u002F Check if metadata-less item is a svg file. Just the main file to replace all thumbnails since SVG's don't need thumbnails.\n-\t\t\tif (strpos($this->target_url, '.svg') !== false)\n-\t\t\t{\n-\t\t\t$svg_file = wp_basename($this->target_url);\n-\t\t\treturn $svg_file;  \u002F\u002F this is the relpath of the mainfile.\n-\t\t\t}\n-\n \t\t\t$old_width = $this->source_metadata['sizes'][$sizeName]['width']; \u002F\u002F the width from size not in new image\n \t\t\t$new_width = $this->target_metadata['width']; \u002F\u002F default check - the width of the main image\n \ndiff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.1\u002Fclasses\u002Femr-plugin.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.2\u002Fclasses\u002Femr-plugin.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.1\u002Fclasses\u002Femr-plugin.php\t2026-06-17 09:46:32.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fenable-media-replace\u002F4.2.2\u002Fclasses\u002Femr-plugin.php\t2026-06-28 20:49:46.000000000 +0000\n@@ -608,7 +608,7 @@\n             $modified = date_i18n(__('M j, Y @ H:i'), strtotime($post->post_modified));\n             ?>\n         \u003Cdiv class=\"misc-pub-section curtime\">\n-            \u003Cspan id=\"timestamp\">\u003C?php echo esc_html__('Revised', 'enable-media-replace'); ?>: \u003Cb>\u003C?php echo $modified; ?>\u003C\u002Fb>\u003C\u002Fspan>\n+            \u003Cspan id=\"timestamp\">\u003C?php echo esc_html__('Revised', 'enable-media-replace'); ?>: \u003Cb>\u003C?php echo esc_html($modified); ?>\u003C\u002Fb>\u003C\u002Fspan>\n         \u003C\u002Fdiv>\n \n             \u003C?php\n@@ -619,7 +619,7 @@\n             $display_name = get_the_author_meta('display_name', $author_id);\n             ?>\n       \u003Cdiv class=\"misc-pub-section replace_author\">\n-        \u003Cspan>\u003C?php echo esc_html__('Replaced By', 'enable-media-replace'); ?>: \u003Cb>\u003C?php echo $display_name; ?>\u003C\u002Fb>\u003C\u002Fspan>\n+        \u003Cspan>\u003C?php echo esc_html__('Replaced By', 'enable-media-replace'); ?>: \u003Cb>\u003C?php echo esc_html($display_name); ?>\u003C\u002Fb>\u003C\u002Fspan>\n       \u003C\u002Fdiv>\n             \u003C?php\n         }","The exploit is achieved by an Editor-level user uploading a malicious replacement file. The attacker navigates to the 'Replace Media' screen for an existing attachment, selects Option 2 ('Replace the file, use the new file name, and update all links'), and uploads a file with a crafted filename containing an XSS payload (e.g., `payload\">\u003Cscript>alert(origin)\u003C\u002Fscript>.png`). Because the plugin uses direct SQL queries via $wpdb to update links in the `wp_posts` table, it bypasses WordPress's standard content sanitization. When the updated post is viewed on the frontend, the script executes. Additionally, the attacker's display name, if malicious, is rendered unescaped on the 'Edit Media' admin screen after the replacement occurs.","gemini-3-flash-preview","2026-07-25 12:13:10","2026-07-25 12:14:36",{"type":40,"vulnerable_version":41,"fixed_version":11,"vulnerable_browse":42,"vulnerable_zip":43,"fixed_browse":44,"fixed_zip":45,"all_tags":46},"plugin","4.2.1","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fenable-media-replace\u002Ftags\u002F4.2.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fenable-media-replace.4.2.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fenable-media-replace\u002Ftags\u002F4.2.2","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fenable-media-replace.4.2.2.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fenable-media-replace\u002Ftags"]