Keep Backup Daily <= 2.1.2 - Authenticated (Admin+) Stored Cross-Site Scripting via Backup Title
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:NTechnical Details
<=2.1.2What Changed in the Fix
Changes introduced in v2.1.3
Source Code
WordPress.org SVN# 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
- Input Entry: An administrator sends a POST request to
admin-ajax.phpwithaction=update_kbd_bkup_alias. - Processing: The handler (likely in
inc/functions.phporindex.php, though truncated in source) retrieves thevalparameter. - Sanitization: It applies
sanitize_text_field(), which strips HTML tags (e.g.,<script>) but preserves double quotes ("). - Storage: The sanitized string is stored in the WordPress
optionstable under the keykbd_backup_aliasesviaupdate_option(). - Retrieval: When an administrator visits the backup download page (
options-general.php?page=kbd_download), the functionkbd_force_download_old()(ininc/kbd_cron.php) is called. - Sourcing:
kbd_force_download_old()callskbd_backup_aliases($name)to retrieve the stored title for a backup. - Sink: The retrieved
$titleis echoed into an HTML attribute (e.g.,value="..."ortitle="...") withoutesc_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.
- Identify Page: The backup management page is
options-general.php?page=kbd_download. - Search for Localized Data:
- Navigate to the page using
browser_navigate. - Use
browser_evalto check for common plugin objects or localized scripts. - Search for keys like
kbd_ajax,kbd_data, or similar.
- Navigate to the page using
- Check Verification: If the AJAX handler uses
check_ajax_refererorwp_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.
- Trigger a backup via the plugin settings or WP-CLI:
wp eval "kbd_db_backup_process();" - Confirm a file exists in
wp-content/uploadsstarting with the prefix defined inindex.php:database-backup-[DB_NAME]-.
Step 2: Extract a backup ID
- Navigate to
options-general.php?page=kbd_download. - Find the filename of the backup. The "ID" used in the
kbd_backup_aliasesoption is the filename without the prefix and.gzextension (seekbd_force_download_oldlogic).
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_aliasid:[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
- Navigate to
options-general.php?page=kbd_download. - 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
- User: An administrator account.
- Plugin Config: Ensure the plugin is activated.
- Backup File: Create a dummy backup file manually if the plugin process fails:
(Adjust filename to match the expected format used inDB_NAME=$(wp config get DB_NAME) touch "/var/www/html/wp-content/uploads/database-backup-$DB_NAME-12345-1234567890.gz"kbd_backup_url_encode_decode).
7. Expected Results
- The AJAX request should return a success code (e.g.,
1or a JSON success). - The
kbd_backup_aliasesoption 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
- Check Database:
Confirm the payloadwp option get kbd_backup_aliases --format=jsononmouseover="alert(document.domain)"is present. - Inspect DOM: Use
browser_evalto 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:
- List JS files:
find wp-content/plugins/keep-backup-daily -name "*.js" - Grep for
action:in those files to find the correct AJAX action name. - If
sanitize_text_fieldis more aggressive than described, try injecting into different attributes or using different event handlers (e.g.,onfocuswithautofocus).
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
@@ -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'); @@ -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.