CVE-2026-3577

Keep Backup Daily <= 2.1.2 - Authenticated (Admin+) Stored Cross-Site Scripting via Backup Title

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
4.4
CVSS Score
4.4
CVSS Score
medium
Severity
2.1.3
Patched in
1d
Time to patch

Description

The Keep Backup Daily plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the backup title alias (`val` parameter) in the `update_kbd_bkup_alias` AJAX action in all versions up to, and including, 2.1.2. This is due to insufficient input sanitization and output escaping. While `sanitize_text_field()` strips HTML tags on save, it does not encode double quotes. The backup titles are output in HTML attribute contexts without `esc_attr()`. This makes it possible for authenticated attackers, with Administrator-level access and above, to inject arbitrary web scripts via attribute injection that will execute whenever another administrator views the backup list page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.1.2
PublishedMarch 20, 2026
Last updatedMarch 20, 2026
Affected pluginkeep-backup-daily

What Changed in the Fix

Changes introduced in v2.1.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-3577 ## 1. Vulnerability Summary The **Keep Backup Daily** plugin (<= 2.1.2) is vulnerable to **Stored Cross-Site Scripting (XSS)**. The vulnerability exists in the handling of backup title aliases. An administrator can update a backup's display name (alias) …

Show full research plan

Exploitation Research Plan - CVE-2026-3577

1. Vulnerability Summary

The Keep Backup Daily plugin (<= 2.1.2) is vulnerable to Stored Cross-Site Scripting (XSS). The vulnerability exists in the handling of backup title aliases. An administrator can update a backup's display name (alias) via an AJAX action. While the input is passed through sanitize_text_field(), this function does not strip or encode double quotes. When these titles are subsequently rendered on the backup management page, they are output within HTML attributes without proper escaping (specifically missing esc_attr()), allowing an attacker to break out of the attribute and inject event handlers or other malicious scripts.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: update_kbd_bkup_alias (inferred from description/typical plugin naming)
  • Vulnerable Parameter: val (the new alias string)
  • ID Parameter: id (the filename/key of the backup to update)
  • Authentication: Required (Administrator level)
  • Precondition: At least one backup must exist in the system to provide a target id (filename).

3. Code Flow

  1. Input Entry: An administrator sends a POST request to admin-ajax.php with action=update_kbd_bkup_alias.
  2. Processing: The handler (likely in inc/functions.php or index.php, though truncated in source) retrieves the val parameter.
  3. Sanitization: It applies sanitize_text_field(), which strips HTML tags (e.g., <script>) but preserves double quotes (").
  4. Storage: The sanitized string is stored in the WordPress options table under the key kbd_backup_aliases via update_option().
  5. Retrieval: When an administrator visits the backup download page (options-general.php?page=kbd_download), the function kbd_force_download_old() (in inc/kbd_cron.php) is called.
  6. Sourcing: kbd_force_download_old() calls kbd_backup_aliases($name) to retrieve the stored title for a backup.
  7. Sink: The retrieved $title is echoed into an HTML attribute (e.g., value="..." or title="...") without esc_attr().

4. Nonce Acquisition Strategy

The source code provided does not explicitly show the registration of the update_kbd_bkup_alias action or its nonce. However, inc/functions.php contains a register_kbd_styles hook. We will investigate if the plugin localizes a nonce for its AJAX operations.

  1. Identify Page: The backup management page is options-general.php?page=kbd_download.
  2. Search for Localized Data:
    • Navigate to the page using browser_navigate.
    • Use browser_eval to check for common plugin objects or localized scripts.
    • Search for keys like kbd_ajax, kbd_data, or similar.
  3. Check Verification: If the AJAX handler uses check_ajax_referer or wp_verify_nonce, the action string is likely related to the plugin name or the specific operation (e.g., 'kbd_nonce', 'kbd_update_alias').

5. Exploitation Strategy

Step 1: Ensure a backup exists

Before an alias can be updated, a backup file must be present in the uploads directory.

  1. Trigger a backup via the plugin settings or WP-CLI: wp eval "kbd_db_backup_process();"
  2. Confirm a file exists in wp-content/uploads starting with the prefix defined in index.php: database-backup-[DB_NAME]-.

Step 2: Extract a backup ID

  1. Navigate to options-general.php?page=kbd_download.
  2. Find the filename of the backup. The "ID" used in the kbd_backup_aliases option is the filename without the prefix and .gz extension (see kbd_force_download_old logic).

Step 3: Inject the XSS Payload

Send the malicious AJAX request.

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Body:
    • action: update_kbd_bkup_alias
    • id: [BACKUP_FILENAME_KEY]
    • val: alias" onmouseover="alert(document.domain)" style="display:block;width:100%;height:100px;background:red;"
  • Headers: Content-Type: application/x-www-form-urlencoded

Step 4: Trigger Execution

  1. Navigate to options-general.php?page=kbd_download.
  2. The payload will be rendered inside an attribute. If using onmouseover, moving the mouse over the input/element will trigger the alert.

6. Test Data Setup

  1. User: An administrator account.
  2. Plugin Config: Ensure the plugin is activated.
  3. Backup File: Create a dummy backup file manually if the plugin process fails:
    DB_NAME=$(wp config get DB_NAME)
    touch "/var/www/html/wp-content/uploads/database-backup-$DB_NAME-12345-1234567890.gz"
    
    (Adjust filename to match the expected format used in kbd_backup_url_encode_decode).

7. Expected Results

  • The AJAX request should return a success code (e.g., 1 or a JSON success).
  • The kbd_backup_aliases option in the database should contain the payload.
  • When viewing the backup list page, the HTML source should look like:
    <input ... value="alias" onmouseover="alert(document.domain)" ...>.

8. Verification Steps

  1. Check Database:
    wp option get kbd_backup_aliases --format=json
    
    Confirm the payload onmouseover="alert(document.domain)" is present.
  2. Inspect DOM: Use browser_eval to find the element and check for the injected attribute:
    document.querySelector('input.kbd-bkup-alias').onmouseover.toString()
    

9. Alternative Approaches

If the update_kbd_bkup_alias action is not the correct name, check for any jQuery.post calls in the plugin's JS assets:

  1. List JS files: find wp-content/plugins/keep-backup-daily -name "*.js"
  2. Grep for action: in those files to find the correct AJAX action name.
  3. If sanitize_text_field is more aggressive than described, try injecting into different attributes or using different event handlers (e.g., onfocus with autofocus).
Research Findings
Static analysis — not yet PoC-verified

Summary

The Keep Backup Daily plugin is vulnerable to Stored Cross-Site Scripting because it fails to properly escape backup titles when rendering them in HTML attribute contexts. An administrator can inject malicious JavaScript using double quotes to break out of attributes like 'value' or 'title', which will execute when any administrator views the backup management page.

Vulnerable Code

// inc/functions.php (The save handler misses proper escaping for attribute context)
function update_kbd_bkup_alias() {
    global $wpdb, $kbd_backup_aliases, $kbd_db_prefix;

    if(isset($_POST['key']) && $_POST['key']!='' && isset($_POST['val']) && $_POST['val']!=''){
        $kbd_backup_aliases[$_POST['key']] = sanitize_text_field($_POST['val']);
        update_option('kbd_backup_aliases', $kbd_backup_aliases);
        echo 1;
    }
    exit;
}

---

// inc/kbd_cron.php around lines 282 and 505 (The rendering context misses esc_attr)
echo '<li>';
echo '<input type="text" value="'.$title.'" />';
echo '<a title="'.__('Click here to edit this title','wpkbd').'" class="kbd-bkup-title" data-key="'.$name.'">'.$title.'</a>';

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/keep-backup-daily/2.1.2/inc/functions.php /home/deploy/wp-safety.org/data/plugin-versions/keep-backup-daily/2.1.3/inc/functions.php
--- /home/deploy/wp-safety.org/data/plugin-versions/keep-backup-daily/2.1.2/inc/functions.php	2026-02-28 07:14:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/keep-backup-daily/2.1.3/inc/functions.php	2026-03-13 00:17:58.000000000 +0000
@@ -578,6 +578,9 @@
 	add_action( 'wp_ajax_update_kbd_bkup_alias', 'update_kbd_bkup_alias' );
 	
 	function update_kbd_bkup_alias() {
+		if ( ! current_user_can('manage_options') ) {
+			wp_send_json_error('Insufficient permissions.');
+		}
 		global $wpdb, $kbd_backup_aliases, $kbd_db_prefix; // this is how you get access to the database
 	
 		if(isset($_POST['key']) && $_POST['key']!='' && isset($_POST['val']) && $_POST['val']!=''){
@@ -834,6 +837,13 @@
 					$kbd_path = sanitize_kbd_data($_POST['kbd_path']);
 					$kbd_path= str_replace('\\', '/', $kbd_path);
 					$kbd_path= str_replace('\\', '/', $kbd_path);
+										
+					$allowed_directory = realpath(WP_CONTENT_DIR . '/uploads');
+					$real_path = realpath($kbd_path);
+					
+					if ($real_path === false || strpos($real_path, $allowed_directory) !== 0) {
+						wp_send_json_error('Invalid directory path.');
+					}					
 
 					kbd_get_dir_list_html($kbd_path, 'sub');
 
--- /home/deploy/wp-safety.org/data/plugin-versions/keep-backup-daily/2.1.2/inc/kbd_cron.php	2026-02-28 07:14:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/keep-backup-daily/2.1.3/inc/kbd_cron.php	2026-03-13 00:17:58.000000000 +0000
@@ -279,8 +279,8 @@
 						$stats = $db_size;
 						
 						echo '<li>';
-						echo '<input type="text" value="'.$title.'" />';
-						echo '<a title="'.__('Click here to edit this title','wpkbd').'" class="kbd-bkup-title" data-key="'.$name.'">'.$title.'</a>';
+						echo '<input type="text" value="' . esc_attr($title) . '" />';
+						echo '<a title="' . esc_attr(__('Click here to edit this title','wpkbd')) . '" class="kbd-bkup-title" data-key="' . esc_attr($name) . '">' . esc_html($title) . '</a>';
 						echo '<a style="margin-left:100px; font-size:12px; color:blue;" href="'.$file.'" >'.__('Download','wpkbd').'</a>';
 						echo '<a style="margin-left:100px; font-size:12px; color:red;" href="'.$file.'&rm">'.__('Delete','wpkbd').'</a>';
 						echo '<span style="margin-left:100px">'.($b == 1 ? '[LATEST] ' : '').$stats.'</span>';
@@ -502,8 +502,8 @@
 				$stats = $db_size;
 				
 				echo '<li>';
-				echo '<input type="text" value="'.$title.'" />';
-				echo '<a title="'.__('Click here to edit this title','wpkbd').'" class="kbd-bkup-title" data-key="'.$name.'">'.$title.'</a>';
+				echo '<input type="text" value="' . esc_attr($title) . '" />';
+				echo '<a title="' . esc_attr(__('Click here to edit this title','wpkbd')) . '" class="kbd-bkup-title" data-key="' . esc_attr($name) . '">' . esc_html($title) . '</a>';
 				echo '<a style="margin-left:100px; font-size:12px; color:blue;" href="'.$file.'" >'.__('Download','wpkbd').'</a>';
 				echo '<a class="kbd_del_backup" style="margin-left:100px; font-size:12px; color:red;" href="'.$file.'&rm">'.__('Delete','wpkbd').'</a>';
 				echo '<span style="margin-left:100px">'.($b == 1 ? '[LATEST] ' : '').$stats.'</span>';

Exploit Outline

1. Authenticate as a WordPress Administrator. 2. Ensure at least one backup exists in the system (this provides the necessary backup ID/key). 3. Send a POST request to `/wp-admin/admin-ajax.php` with the action `update_kbd_bkup_alias`. 4. Include a payload in the `val` parameter that uses double quotes to break out of an HTML attribute, such as: `alias" onmouseover="alert(document.cookie)"`. 5. Include the target backup filename in the `key` parameter. 6. Navigate to the backup download page (`options-general.php?page=kbd_download`). The payload will be rendered as an attribute within an `<input>` and `<a>` tag, executing the script when the administrator interacts with (or in some cases just loads) the page.

Check if your site is affected.

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