CVE-2025-12656

Migration, Backup, Staging – WPvivid Backup & Migration <= 0.9.128 - Authenticated (Admin+) Arbitrary Directory Deletion

lowExternal Control of File Name or Path
3.8
CVSS Score
3.8
CVSS Score
low
Severity
0.9.129
Patched in
1d
Time to patch

Description

The Migration, Backup, Staging – WPvivid Backup & Migration plugin for WordPress is vulnerable to arbitrary directory deletion due to insufficient file path validation in the delete_cancel_staging_site() function in all versions up to, and including, 0.9.128. This makes it possible for authenticated attackers, with Administrator-level access and above, to delete arbitrary folders on the server, which leads to a loss of data.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=0.9.128
PublishedJune 5, 2026
Last updatedJune 5, 2026
Affected pluginwpvivid-backuprestore

What Changed in the Fix

Changes introduced in v0.9.129

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2025-12656 (Arbitrary Directory Deletion) ## 1. Vulnerability Summary **CVE-2025-12656** is an arbitrary directory deletion vulnerability in the **WPvivid Backup & Migration** plugin (<= 0.9.128). The vulnerability exists within the `delete_cancel_staging_site()` f…

Show full research plan

Exploitation Research Plan: CVE-2025-12656 (Arbitrary Directory Deletion)

1. Vulnerability Summary

CVE-2025-12656 is an arbitrary directory deletion vulnerability in the WPvivid Backup & Migration plugin (<= 0.9.128). The vulnerability exists within the delete_cancel_staging_site() function in includes/staging/class-wpvivid-staging.php. Due to a lack of sufficient path validation, an authenticated user with Administrator-level privileges can provide a manipulated file path that causes the plugin to recursively delete an arbitrary folder on the server. Although it requires Admin privileges, it represents a significant data loss risk and can be used to disable security plugins or delete critical system directories.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wpvivid_delete_cancel_staging_site (inferred based on function name)
  • HTTP Method: POST
  • Vulnerable Parameter: path (inferred) or dir (inferred)
  • Authentication: Required (Administrator)
  • Preconditions: The plugin must be active, and the attacker must have valid administrator credentials.

3. Code Flow

  1. Registration: The WPvivid_Staging_Free class constructor (in includes/staging/class-wpvivid-staging.php) calls $this->load_ajax().
  2. AJAX Handler: Inside load_ajax(), the plugin registers the callback:
    add_action('wp_ajax_wpvivid_delete_cancel_staging_site', array($this, 'delete_cancel_staging_site')); (inferred registration pattern).
  3. Vulnerable Function: The delete_cancel_staging_site() function is triggered.
  4. Input Extraction: The function retrieves a directory path from the user input (e.g., $_POST['path']).
  5. Insufficient Validation: The function fails to check if the provided path is contained within the authorized staging directory (e.g., it does not check if the path starts with WPVIVID_STAGING_DIR or similar).
  6. Sink: The path is passed to a recursive deletion routine, likely WPvivid_Staging_Copy_Files_Ex::delete_dir($path) or a standard PHP rmdir() wrapper, which deletes the entire directory tree at that location.

4. Nonce Acquisition Strategy

The plugin likely protects staging operations with a WordPress nonce.

  1. Identify Script Localization: The function enqueue_scripts in class-wpvivid-staging.php (Line 52) enqueues scripts that handle the staging UI. This is where the nonce is typically localized.
  2. Create Trigger Content: To ensure the staging scripts and nonces are loaded, create a page that includes any WPvivid staging-related shortcode (if applicable) or simply navigate to the plugin's staging page.
    • wp post create --post_type=page --post_status=publish --post_content='[wpvivid_staging_placeholder]' (inferred shortcode)
  3. Navigate and Extract:
    • Login as Administrator using browser_navigate.
    • Navigate to the WPvivid Staging page: /wp-admin/admin.php?page=wpvivid-staging.
    • JS Variable: Look for the localized object, likely wpvivid_staging_vars or wpvivid_ajax_object.
    • Execution: browser_eval("wpvivid_staging_vars.nonce") (inferred name).

5. Exploitation Strategy

The goal is to delete a non-staging directory, such as a dummy folder in uploads.

  • Step 1: Prepare a target directory.
  • Step 2: Obtain a valid Admin session and Nonce.
  • Step 3: Construct the malicious AJAX request.

Request Details:

  • URL: https://<target>/wp-admin/admin-ajax.php
  • Headers:
    • Content-Type: application/x-www-form-urlencoded
    • Cookie: <Admin Cookies>
  • Payload:
    action=wpvivid_delete_cancel_staging_site&path=../../uploads/target_to_delete&_wpnonce=<NONCE>
    
    (Note: The path may need to be an absolute path if the plugin uses ABSPATH, or relative if it uses the current working directory).

6. Test Data Setup

  1. Install Plugin: Ensure WPvivid version 0.9.128 is installed and active.
  2. Create Admin User:
    wp user create exploit_admin admin@example.com --role=administrator --user_pass=password123
  3. Create Target Directory:
    mkdir -p /var/www/html/wp-content/uploads/delete_me_test/
    touch /var/www/html/wp-content/uploads/delete_me_test/evidence.txt

7. Expected Results

  • HTTP Response: The server should return a JSON success response (e.g., {"result":"success"}) or a 1 (standard WP AJAX success).
  • Filesystem Impact: The directory /var/www/html/wp-content/uploads/delete_me_test/ and its contents should be completely removed from the server.

8. Verification Steps

After sending the HTTP request, verify the deletion using WP-CLI:

# Check if the directory still exists
if [ ! -d "/var/www/html/wp-content/uploads/delete_me_test/" ]; then
    echo "Exploit Successful: Directory deleted."
else
    echo "Exploit Failed: Directory still exists."
fi

9. Alternative Approaches

  • Absolute Path Injection: If relative pathing (../) is filtered but the validation is still weak, try providing the full absolute path retrieved via other leaks (e.g., phpinfo or error logs): /var/www/html/wp-content/plugins/akismet/.
  • Different AJAX Action: If wpvivid_delete_cancel_staging_site is not the correct action name, check class-wpvivid-staging-sites-list.php for actions related to "removing" or "cleaning up" staging sites, such as wpvivid_delete_staging_site.
  • Bypass via Directory Traversal: If the plugin prepends a base path, use traversal sequences: path=/wp-content/wpvividbackups/staging/../../../wp-content/plugins/some-plugin.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WPvivid Backup & Migration plugin for WordPress is vulnerable to arbitrary directory deletion in versions up to 0.9.128. This occurs because the `delete_cancel_staging_site()` function lacks sufficient validation to ensure that the directory being deleted is actually a staging site, allowing an authenticated administrator to recursively delete any folder on the server.

Vulnerable Code

/* includes/staging/class-wpvivid-staging.php */

    public function delete_cancel_staging_site(){
        global $wpvivid_plugin;
        check_ajax_referer( 'wpvivid_ajax', 'nonce' );

        // ... [retrieval of site_path from staging info] ...

                if (!empty($site_path)) {
                    $home_path = untrailingslashit(ABSPATH);
                    if ($home_path != $site_path) {
                        if (file_exists($site_path)) {
                            if (!class_exists('WP_Filesystem_Base')) include_once(ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php');
                            if (!class_exists('WP_Filesystem_Direct')) include_once(ABSPATH . '/wp-admin/includes/class-wp-filesystem-direct.php');

                            $wp_filesystem = new WP_Filesystem_Direct(array());

                            WPvivid_Staging_Copy_Files_Ex::delete_dir($site_path);
                        }
                    }
                }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.128/includes/staging/class-wpvivid-staging.php /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.129/includes/staging/class-wpvivid-staging.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.128/includes/staging/class-wpvivid-staging.php	2025-11-18 00:25:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpvivid-backuprestore/0.9.129/includes/staging/class-wpvivid-staging.php	2026-06-01 00:32:46.000000000 +0000
@@ -1264,6 +1264,126 @@
         }
     }
 
+    private function wpvivid_normalize_delete_path($path, $use_realpath = true)
+    {
+        if (empty($path) || !is_string($path)) {
+            return false;
+        }
+
+        $path = wp_normalize_path($path);
+        $path = untrailingslashit($path);
+        if ($use_realpath) {
+            $real_path = realpath($path);
+            if ($real_path !== false) {
+                $path = wp_normalize_path($real_path);
+                $path = untrailingslashit($path);
+            }
+        }
+
+        if ((defined('PHP_OS_FAMILY') && PHP_OS_FAMILY === 'Windows') || DIRECTORY_SEPARATOR === '\\') {
+            $path = strtolower($path);
+        }
+
+        return $path;
+    }
+
+    private function wpvivid_get_cancel_staging_cleanup($site_path)
+    {
+        $normalized_path = $this->wpvivid_normalize_delete_path($site_path, false);
+        if ($normalized_path === false) {
+            return false;
+        }
+
+        $cleanup = get_transient('wpvivid_cancel_staging_cleanup_' . md5($normalized_path));
+        if (!is_array($cleanup) || empty($cleanup['staging_path'])) {
+            return false;
+        }
+
+        $cleanup_path = $this->wpvivid_normalize_delete_path($cleanup['staging_path'], false);
+        $delete_path = $this->wpvivid_normalize_delete_path($site_path, false);
+        if ($cleanup_path === false || $delete_path === false || $cleanup_path !== $delete_path) {
+            return false;
+        }
+
+        return $cleanup;
+    }
+
+    private function wpvivid_is_allowed_staging_delete_path($site_path)
+    {
+        if (empty($site_path) || !is_string($site_path)) {
+            return false;
+        }
+
+        if (!file_exists($site_path) || !is_dir($site_path)) {
+            return false;
+        }
+
+        $real_site_path = $this->wpvivid_normalize_delete_path($site_path, true);
+        if ($real_site_path === false) {
+            return false;
+        }
+
+        $real_site_path = wp_normalize_path(untrailingslashit($real_site_path));
+
+        $protected_paths = array(
+            ABSPATH,
+            WP_CONTENT_DIR,
+            WP_PLUGIN_DIR,
+            get_theme_root(),
+            ABSPATH . 'wp-admin',
+            ABSPATH . 'wp-includes'
+        );
+
+        $upload_dir = wp_get_upload_dir();
+        if (!empty($upload_dir['basedir'])) {
+            $protected_paths[] = $upload_dir['basedir'];
+        }
+
+        foreach ($protected_paths as $protected_path) {
+            $real_protected_path = $this->wpvivid_normalize_delete_path($protected_path, true);
+            if ($real_protected_path === false) {
+                continue;
+            }
+
+            if ($real_site_path === $real_protected_path) {
+                return false;
+            }
+
+            if (strpos($real_protected_path . '/', $real_site_path . '/') === 0) {
+                return false;
+            }
+        }
+
+        $tasks = get_option('wpvivid_staging_task_list', array());
+        if (is_array($tasks)) {
+            foreach ($tasks as $task) {
+                $allowed_paths = array();
+
+                if (!empty($task['path']['des_path'])) {
+                    $allowed_paths[] = $task['path']['des_path'];
+                }
+
+                if (!empty($task['site']['path'])) {
+                    $allowed_paths[] = $task['site']['path'];
+                }
+
+                foreach ($allowed_paths as $allowed_path) {
+                    $real_allowed_path = $this->wpvivid_normalize_delete_path($allowed_path, true);
+                    if ($real_allowed_path !== false && $real_site_path === $real_allowed_path) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        $cleanup = $this->wpvivid_get_cancel_staging_cleanup($site_path);
+        if ($cleanup !== false) {
+            return true;
+        }
+
+        return false;
+    }
+
     public function delete_cancel_staging_site(){
         global $wpvivid_plugin;
         check_ajax_referer( 'wpvivid_ajax', 'nonce' );
@@ -1285,9 +1405,18 @@
                 $db_name = $staging_site_info['staging_additional_db_name'];
                 $db_host = $staging_site_info['staging_additional_db_host'];
                 if (!empty($site_path)) {
-                    $home_path = untrailingslashit(ABSPATH);
-                    if ($home_path != $site_path) {
-                        if (file_exists($site_path)) {
+                    if (file_exists($site_path)) {
+                        if (!$this->wpvivid_is_allowed_staging_delete_path($site_path)) {
+                            echo wp_json_encode(array(
+                                'result' => 'failed',
+                                'error'  => 'Invalid staging path.'
+                            ));
+                            die();
+                        }
+
+                        $home_path = untrailingslashit(ABSPATH);
+                        if ($home_path != $site_path) {
+
                             if (!class_exists('WP_Filesystem_Base')) include_once(ABSPATH . '/wp-admin/includes/class-wp-filesystem-base.php');
                             if (!class_exists('WP_Filesystem_Direct')) include_once(ABSPATH . '/wp-admin/includes/class-wp-filesystem-direct.php');
 
@@ -1311,6 +1440,11 @@
                     }
                 }
 
+                $normalized_path = $this->wpvivid_normalize_delete_path($site_path, false);
+                if ($normalized_path !== false) {
+                    delete_transient('wpvivid_cancel_staging_cleanup_' . md5($normalized_path));
+                }
+
                 $ret['result'] = 'success';
                 echo wp_json_encode($ret);
             }
@@ -1751,6 +1885,27 @@
                         $ret['staging_table_prefix']=$value['db_connect']['new_prefix'];
                     }
                 }
+
+                if (!empty($ret['staging_path'])) {
+                    $normalized_cleanup_path = $this->wpvivid_normalize_delete_path($ret['staging_path'], false);
+
+                    if ($normalized_cleanup_path !== false) {
+                        set_transient(
+                            'wpvivid_cancel_staging_cleanup_' . md5($normalized_cleanup_path),
+                            array(
+                                'staging_path' => $ret['staging_path'],
+                                'staging_table_prefix' => isset($ret['staging_table_prefix']) ? $ret['staging_table_prefix'] : '',
+                                'staging_additional_db' => isset($ret['staging_additional_db']) ? $ret['staging_additional_db'] : 0,
+                                'staging_additional_db_user' => isset($ret['staging_additional_db_user']) ? $ret['staging_additional_db_user'] : '',
+                                'staging_additional_db_pass' => isset($ret['staging_additional_db_pass']) ? $ret['staging_additional_db_pass'] : '',
+                                'staging_additional_db_name' => isset($ret['staging_additional_db_name']) ? $ret['staging_additional_db_name'] : '',
+                                'staging_additional_db_host' => isset($ret['staging_additional_db_host']) ? $ret['staging_additional_db_host'] : ''
+                            ),
+                            10 * MINUTE_IN_SECONDS
+                        );
+                    }
+                }
+
                 update_option('wpvivid_staging_task_cancel', false, 'no');
                 $b_delete=true;
             }

Exploit Outline

The exploit requires an authenticated user with Administrator privileges. 1. The attacker obtains a valid WordPress nonce for the 'wpvivid_ajax' action, typically found localized in the plugin's staging settings page. 2. The attacker triggers the 'wp_ajax_wpvivid_delete_cancel_staging_site' AJAX action. 3. In vulnerable versions, the plugin attempts to clean up staging sites by deleting the folder specified in the staging site configuration or transient data. 4. By manipulating the staging site path information (either through a previous configuration step or by influencing the input variables), the attacker can specify an arbitrary directory path (e.g., '/wp-content/plugins/'). 5. The plugin recursively deletes the target directory because it only checks if the path is strictly different from the site's home path (ABSPATH), allowing for a full loss of data for that directory.

Check if your site is affected.

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