CVE-2025-13910

WP-WebAuthn <= 1.3.4 - Unauthenticated Stored Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.1
CVSS Score
6.1
CVSS Score
medium
Severity
Unpatched
Patched in
N/A
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
Required
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.3.4
PublishedMarch 20, 2026
Last updatedApril 15, 2026
Affected pluginwp-webauthn
Research Plan
Unverified

# 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 both wp_ajax_ and wp_ajax_nopriv_)
  • Vulnerable Parameter: Likely username, id, or components within the WebAuthn response (e.g., clientDataJSON fields if decoded and logged). Based on typical logging patterns for this plugin, the username or 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)

  1. Entry: An unauthenticated request hits admin-ajax.php?action=wwa_auth.
  2. Handler: The WP_WebAuthn_Handler::wwa_auth() (inferred) method is called.
  3. Logging Trigger: If get_option('wp_webauthn_settings')['logging_enabled'] is true, the plugin calls a logging function (e.g., WP_WebAuthn_Logger::log()).
  4. Storage: User-provided parameters (like a malicious username or malformed credential ID) are passed directly into a $wpdb->insert() call into the wp_wwa_logs (inferred) table without sanitize_text_field.
  5. Sink: An administrator visits the plugin's log page (e.g., wp-admin/admin.php?page=wp-webauthn-logs).
  6. Execution: The log entries are retrieved and echoed directly: echo $log->user_input; (inferred) without esc_html() or esc_attr().

4. Nonce Acquisition Strategy

The wwa_auth action typically requires a nonce localized by the plugin for the frontend login/registration forms.

  1. Identify Shortcode: The plugin uses the shortcode [wp-webauthn] (inferred) or [wwa_login] (inferred) to render WebAuthn interfaces.
  2. Setup Page: Create a public page containing this shortcode to force the plugin to enqueue its scripts and nonces.
  3. Browser Navigation: Use browser_navigate to visit the created page.
  4. Extract Nonce: The plugin likely uses wp_localize_script. Search for the global object, usually named wwa_vars or wp_webauthn_vars.
    • JS Script Key: wwa_vars (inferred)
    • Nonce Key: nonce or wwa_auth_nonce (inferred)
    • Action String: The nonce is likely created with wp_create_nonce('wwa_auth').

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_enabled or log (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_auth
    • nonce: <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

  1. 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
    
  2. 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

  1. HTTP Response: The admin-ajax.php call may return a failure (e.g., {"success":false}) because the WebAuthn handshake isn't completed, but the attempt should be logged.
  2. Database State: A new row in the wp_wwa_logs table containing the <script> payload.
  3. XSS Trigger: When the admin accesses the logs page, a browser alert with the domain name appears.

8. Verification Steps

  1. Check Database:
    wp db query "SELECT * FROM wp_wwa_logs WHERE user_login LIKE '%script%';"
    
    (Note: Replace wp_wwa_logs and user_login with actual table/column names found during discovery).
  2. Confirm Output in Admin:
    Use browser_navigate to the log page and check for the presence of the unescaped script tags in the HTML source.

9. Alternative Approaches

  • Parameter Variation: If username is sanitized, attempt injection via the id field or a custom user-agent header 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_auth handler calls check_ajax_referer with the $die argument set to false without checking the return value, allowing exploitation without a valid nonce.
Research Findings
Static analysis — not yet PoC-verified

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

--- a/inc/Handler.php
+++ b/inc/Handler.php
@@ -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']) {
--- a/inc/Admin/Logs.php
+++ b/inc/Admin/Logs.php
@@ -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.