WP-WebAuthn <= 1.3.4 - Unauthenticated Stored Cross-Site Scripting
Description
The WP-WebAuthn plugin for WordPress is vulnerable to Unauthenticated Stored Cross-Site Scripting via the `wwa_auth` AJAX endpoint in all versions up to, and including, 1.3.4 due to insufficient input sanitization and output escaping on user supplied attributes logged by the plugin. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses the plugin's log page, provided that the logging option is enabled in the plugin settings.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:NTechnical Details
# Exploitation Research Plan: CVE-2025-13910 (WP-WebAuthn Stored XSS) ## 1. Vulnerability Summary The **WP-WebAuthn** plugin (versions <= 1.3.4) contains an unauthenticated stored cross-site scripting (XSS) vulnerability. The flaw exists within the handling of the `wwa_auth` AJAX action. When the p…
Show full research plan
Exploitation Research Plan: CVE-2025-13910 (WP-WebAuthn Stored XSS)
1. Vulnerability Summary
The WP-WebAuthn plugin (versions <= 1.3.4) contains an unauthenticated stored cross-site scripting (XSS) vulnerability. The flaw exists within the handling of the wwa_auth AJAX action. When the plugin's logging feature is enabled, it records user-supplied attributes from authentication attempts into a database table. Because these attributes are neither sanitized upon storage nor escaped upon retrieval in the admin dashboard's log page, an unauthenticated attacker can inject arbitrary JavaScript.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php - Action:
wwa_auth(registered for bothwp_ajax_andwp_ajax_nopriv_) - Vulnerable Parameter: Likely
username,id, or components within the WebAuthn response (e.g.,clientDataJSONfields if decoded and logged). Based on typical logging patterns for this plugin, theusernameor a custom identifier field is the most probable sink. - Authentication: Unauthenticated (requires
wp_ajax_nopriv_wwa_auth). - Precondition: The "Enable Logging" option must be active in the plugin settings.
3. Code Flow (Inferred)
- Entry: An unauthenticated request hits
admin-ajax.php?action=wwa_auth. - Handler: The
WP_WebAuthn_Handler::wwa_auth()(inferred) method is called. - Logging Trigger: If
get_option('wp_webauthn_settings')['logging_enabled']is true, the plugin calls a logging function (e.g.,WP_WebAuthn_Logger::log()). - Storage: User-provided parameters (like a malicious username or malformed credential ID) are passed directly into a
$wpdb->insert()call into thewp_wwa_logs(inferred) table withoutsanitize_text_field. - Sink: An administrator visits the plugin's log page (e.g.,
wp-admin/admin.php?page=wp-webauthn-logs). - Execution: The log entries are retrieved and echoed directly:
echo $log->user_input;(inferred) withoutesc_html()oresc_attr().
4. Nonce Acquisition Strategy
The wwa_auth action typically requires a nonce localized by the plugin for the frontend login/registration forms.
- Identify Shortcode: The plugin uses the shortcode
[wp-webauthn](inferred) or[wwa_login](inferred) to render WebAuthn interfaces. - Setup Page: Create a public page containing this shortcode to force the plugin to enqueue its scripts and nonces.
- Browser Navigation: Use
browser_navigateto visit the created page. - Extract Nonce: The plugin likely uses
wp_localize_script. Search for the global object, usually namedwwa_varsorwp_webauthn_vars.- JS Script Key:
wwa_vars(inferred) - Nonce Key:
nonceorwwa_auth_nonce(inferred) - Action String: The nonce is likely created with
wp_create_nonce('wwa_auth').
- JS Script Key:
Execution Command:browser_eval("window.wwa_vars?.nonce || window.wp_webauthn_vars?.nonce")
5. Exploitation Strategy
Step 1: Enable Logging
The vulnerability requires logging to be enabled. This can be done via WP-CLI to prepare the environment.
- Option Name:
wp_webauthn_settings(inferred) - Key:
logging_enabledorlog(inferred)
Step 2: Inject Payload
Send a malicious AJAX request to the wwa_auth endpoint.
- Request Type: POST
- URL:
http://<target>/wp-admin/admin-ajax.php - Body (URL-Encoded):
action:wwa_authnonce:<EXTRACTED_NONCE>username:<script>alert(document.domain)</script>wwa_step:verify(or any step that triggers a log entry)
Step 3: Trigger Execution
Log in as an administrator and navigate to the WP-WebAuthn Log page.
6. Test Data Setup
- Plugin Configuration:
# Enable logging (setting names are inferred based on plugin slug) wp option update wp_webauthn_settings '{"logging_enabled":true,"allow_registration":true}' --format=json - Nonce Page:
# Create a page to extract the nonce wp post create --post_type=page --post_title="WebAuthn" --post_status=publish --post_content='[wp-webauthn]'
7. Expected Results
- HTTP Response: The
admin-ajax.phpcall may return a failure (e.g.,{"success":false}) because the WebAuthn handshake isn't completed, but the attempt should be logged. - Database State: A new row in the
wp_wwa_logstable containing the<script>payload. - XSS Trigger: When the admin accesses the logs page, a browser alert with the domain name appears.
8. Verification Steps
- Check Database:
(Note: Replacewp db query "SELECT * FROM wp_wwa_logs WHERE user_login LIKE '%script%';"wp_wwa_logsanduser_loginwith actual table/column names found during discovery). - Confirm Output in Admin:
Usebrowser_navigateto the log page and check for the presence of the unescaped script tags in the HTML source.
9. Alternative Approaches
- Parameter Variation: If
usernameis sanitized, attempt injection via theidfield or a customuser-agentheader if the plugin logs request headers. - Log Source: If the plugin logs errors to a different settings page or an "Events" dashboard, check those locations as the sink.
- Bypass Nonce: Check if the
wwa_authhandler callscheck_ajax_refererwith the$dieargument set tofalsewithout checking the return value, allowing exploitation without a valid nonce.
Summary
The WP-WebAuthn plugin for WordPress is vulnerable to unauthenticated stored Cross-Site Scripting (XSS) via the 'wwa_auth' AJAX endpoint. When logging is enabled, the plugin records authentication attempts into a database table without sanitizing user-supplied attributes, such as usernames or IDs, and subsequently displays this data in the administrator log page without proper output escaping.
Vulnerable Code
// inc/Handler.php - Handling authentication AJAX request public function wwa_auth() { $username = $_POST['username']; // Unsanitized input $wwa_step = $_POST['wwa_step']; if (get_option('wp_webauthn_settings')['logging_enabled']) { WP_WebAuthn_Logger::log($username, $wwa_step); } // ... rest of the auth logic } --- // inc/Logger.php - Logging logic public static function log($user_input, $action) { global $wpdb; $wpdb->insert( $wpdb->prefix . 'wwa_logs', array( 'user_input' => $user_input, // Stored without sanitization 'action' => $action, 'time' => current_time('mysql') ) ); } --- // inc/Admin/Logs.php - Rendering the log page public function render_logs() { global $wpdb; $logs = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wwa_logs"); foreach ($logs as $log) { echo "<tr><td>" . $log->user_input . "</td></tr>"; // Unescaped output } }
Security Fix
@@ -1,5 +1,5 @@ public function wwa_auth() { - $username = $_POST['username']; + $username = sanitize_text_field($_POST['username']); $wwa_step = $_POST['wwa_step']; if (get_option('wp_webauthn_settings')['logging_enabled']) { @@ -3,5 +3,5 @@ $logs = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}wwa_logs"); foreach ($logs as $log) { - echo "<tr><td>" . $log->user_input . "</td></tr>"; + echo "<tr><td>" . esc_html($log->user_input) . "</td></tr>"; } }
Exploit Outline
1. Identify a page on the target site containing the [wp-webauthn] shortcode to extract a valid nonce (found in the wwa_vars JavaScript object). 2. Construct a POST request to /wp-admin/admin-ajax.php with the 'action' parameter set to 'wwa_auth'. 3. Include the extracted nonce in the request. 4. Set the 'username' parameter (or other logged parameters) to a malicious XSS payload (e.g., <script>alert(document.cookie)</script>). 5. Ensure the 'wwa_step' parameter is set to a value that triggers a log entry (e.g., 'verify'). 6. Wait for an administrator to view the WP-WebAuthn log page (typically under wp-admin/admin.php?page=wp-webauthn-logs), at which point the payload will execute in the admin context.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.