From 95960e570db073b62be4f8ff2da71f6f72dc931a Mon Sep 17 00:00:00 2001 From: shewa <30765517+shewa12@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:03:43 +0600 Subject: [PATCH] Fix: Stored XSS in profile display name consent logs and IDOR in quiz builder question deletion --- GDPR/Controllers/UserConsent.php | 5 +- .../admin-dashboard/segments/consent-logs.js | 10 +++- .../js/admin-dashboard/segments/students.js | 10 +++- classes/QuizBuilder.php | 59 ++++++++++++++----- classes/Student.php | 10 ++-- 5 files changed, 67 insertions(+), 27 deletions(-) diff --git a/GDPR/Controllers/UserConsent.php b/GDPR/Controllers/UserConsent.php index 8a9505080e..5fec3c88db 100644 --- a/GDPR/Controllers/UserConsent.php +++ b/GDPR/Controllers/UserConsent.php @@ -110,7 +110,8 @@ public function render_consent_logs_column( $value, $column_name, $user_id ) { return $value; } - $value = ''; + $user_name = sanitize_text_field( wp_strip_all_tags( wp_specialchars_decode( $user->display_name ?? '', ENT_QUOTES ) ) ); + $value = ''; return $value; } @@ -387,7 +388,7 @@ public function render_consent_logs_button( $user_data ): void { return; } - $user_name = $user_data->display_name ?? ''; + $user_name = sanitize_text_field( wp_strip_all_tags( wp_specialchars_decode( $user_data->display_name ?? '', ENT_QUOTES ) ) ); $user_joined = $user_data->user_registered ?? ''; $user_email = $user_data->user_email ?? ''; $user_login = $user_data->user_login ?? ''; diff --git a/assets/src/js/admin-dashboard/segments/consent-logs.js b/assets/src/js/admin-dashboard/segments/consent-logs.js index 375afd5d0b..e6f94c386f 100644 --- a/assets/src/js/admin-dashboard/segments/consent-logs.js +++ b/assets/src/js/admin-dashboard/segments/consent-logs.js @@ -114,10 +114,10 @@ const fetchAndRender = (userId, userName, userJoined, avatarSrc, userEmail, user } const userCard = `
`; @@ -125,6 +125,10 @@ const fetchAndRender = (userId, userName, userJoined, avatarSrc, userEmail, user ${userCard} `; + const cardNameEl = modalBody.querySelector('.tutor-consent-user-card-name'); + if (cardNameEl) cardNameEl.textContent = userName; + const cardJoinedEl = modalBody.querySelector('.tutor-consent-user-card-joined'); + if (cardJoinedEl && userJoined) cardJoinedEl.textContent = `${__('Joined', 'tutor')} ${userJoined}`; if (downloadBtn) downloadBtn.style.display = ''; }) .catch(() => showEmpty()); diff --git a/assets/src/js/admin-dashboard/segments/students.js b/assets/src/js/admin-dashboard/segments/students.js index 5b01c1bb71..4c297aa87e 100644 --- a/assets/src/js/admin-dashboard/segments/students.js +++ b/assets/src/js/admin-dashboard/segments/students.js @@ -95,10 +95,10 @@ const fetchAndRender = (userId, userName, userJoined, avatarSrc, userEmail, user } const userCard = ` `; @@ -106,6 +106,10 @@ const fetchAndRender = (userId, userName, userJoined, avatarSrc, userEmail, user ${userCard} `; + const cardNameEl = modalBody.querySelector('.tutor-consent-user-card-name'); + if (cardNameEl) cardNameEl.textContent = userName; + const cardJoinedEl = modalBody.querySelector('.tutor-consent-user-card-joined'); + if (cardJoinedEl && userJoined) cardJoinedEl.textContent = `${__('Joined', 'tutor')} ${userJoined}`; }) .catch(() => showEmpty()); }; diff --git a/classes/QuizBuilder.php b/classes/QuizBuilder.php index f1f234f905..9fa65f9f68 100644 --- a/classes/QuizBuilder.php +++ b/classes/QuizBuilder.php @@ -377,7 +377,7 @@ public function validate_payload( $payload ) { $errors['ID'][] = __( 'Invalid quiz id provided', 'tutor' ); } - if ( $quiz && ! current_user_can( 'edit_post', $quiz_id ) ) { + if ( $quiz && ( ! current_user_can( 'edit_post', $quiz_id ) || ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) ) { $success = false; $errors['permission'][] = __( 'You do not have permission to edit this quiz', 'tutor' ); } @@ -405,8 +405,11 @@ public function validate_payload( $payload ) { $errors = array_merge( $errors, $validation->errors ); } - if ( ! empty( $questions ) ) { + if ( $quiz_id ) { $this->set_current_quiz_questions_answer_ids( $quiz_id ); + } + + if ( ! empty( $questions ) ) { try { $this->is_valid_quiz_question_answer_payload( $questions ); } catch ( \Throwable $th ) { @@ -482,10 +485,11 @@ public function validate_payload( $payload ) { * @param array $deleted_question_ids question ids. * @param array $deleted_answer_ids answer ids. * @param array $deleted_temp_mask_values unsaved draw/pin/puzzle mask values. + * @param int $quiz_id optional quiz id to scope deletions. * * @return void */ - public function handle_delete( $deleted_question_ids = array(), $deleted_answer_ids = array(), $deleted_temp_mask_values = array() ) { + public function handle_delete( $deleted_question_ids = array(), $deleted_answer_ids = array(), $deleted_temp_mask_values = array(), $quiz_id = 0 ) { global $wpdb; $deleted_question_ids = array_filter( $deleted_question_ids, 'is_numeric' ); $deleted_answer_ids = array_filter( $deleted_answer_ids, 'is_numeric' ); @@ -493,6 +497,33 @@ public function handle_delete( $deleted_question_ids = array(), $deleted_answer_ $question_file_paths = array(); $mask_question_ids = array(); + $quiz_id = (int) $quiz_id; + if ( $quiz_id > 0 ) { + if ( count( $deleted_question_ids ) ) { + $in_clause = QueryHelper::prepare_in_clause( $deleted_question_ids ); + //phpcs:ignore -- sanitized $in_clause. + $valid_question_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d AND question_id IN ({$in_clause})", + $quiz_id + ) + ); + $deleted_question_ids = ! empty( $valid_question_ids ) ? array_map( 'intval', $valid_question_ids ) : array(); + } + + if ( count( $deleted_answer_ids ) ) { + $in_clause = QueryHelper::prepare_in_clause( $deleted_answer_ids ); + //phpcs:ignore -- sanitized $in_clause. + $valid_answer_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT a.answer_id FROM {$wpdb->prefix}tutor_quiz_question_answers a INNER JOIN {$wpdb->prefix}tutor_quiz_questions q ON a.belongs_question_id = q.question_id WHERE q.quiz_id = %d AND a.answer_id IN ({$in_clause})", + $quiz_id + ) + ); + $deleted_answer_ids = ! empty( $valid_answer_ids ) ? array_map( 'intval', $valid_answer_ids ) : array(); + } + } + if ( count( $deleted_question_ids ) ) { $mask_question_ids = $this->get_deletable_mask_question_ids( $deleted_question_ids ); @@ -840,7 +871,7 @@ public function save_quiz( $topic_id, $payload ) { $deleted_answer_ids = Input::post( 'deleted_answer_ids', array(), Input::TYPE_ARRAY ); $deleted_temp_mask_values = Input::post( 'deleted_temp_mask_values', array(), Input::TYPE_ARRAY ); - $this->handle_delete( $deleted_question_ids, $deleted_answer_ids, $deleted_temp_mask_values ); + $this->handle_delete( $deleted_question_ids, $deleted_answer_ids, $deleted_temp_mask_values, $quiz_id ); $wpdb->query( 'COMMIT' ); @@ -902,6 +933,8 @@ public function ajax_quiz_builder_save() { */ private function set_current_quiz_questions_answer_ids( int $quiz_id ) { if ( ! $quiz_id ) { + $this->current_quiz_question_ids = array(); + $this->current_quiz_answer_ids = array(); return; } @@ -912,18 +945,16 @@ private function set_current_quiz_questions_answer_ids( int $quiz_id ) { } } - if ( is_null( $this->current_quiz_question_ids ) && $this->current_quiz ) { - $questions = $this->current_quiz->questions; - if ( $questions ) { - $this->current_quiz_question_ids = wp_list_pluck( $questions, 'question_id' ); - } + if ( is_null( $this->current_quiz_question_ids ) ) { + $questions = $this->current_quiz ? ( $this->current_quiz->questions ?? array() ) : array(); + $this->current_quiz_question_ids = ! empty( $questions ) ? wp_list_pluck( $questions, 'question_id' ) : array(); } - if ( is_null( $this->current_quiz_answer_ids ) && $this->current_quiz ) { - $quiz_details = $this->current_quiz->questions; - - $quiz_question_answer = wp_list_pluck( $quiz_details, 'question_answers' ); - $this->current_quiz_answer_ids = wp_list_pluck( array_merge( ...$quiz_question_answer ), 'answer_id' ); + if ( is_null( $this->current_quiz_answer_ids ) ) { + $questions = $this->current_quiz ? ( $this->current_quiz->questions ?? array() ) : array(); + $quiz_question_answers = ! empty( $questions ) ? wp_list_pluck( $questions, 'question_answers' ) : array(); + $flattened = ! empty( $quiz_question_answers ) ? array_merge( ...$quiz_question_answers ) : array(); + $this->current_quiz_answer_ids = ! empty( $flattened ) ? wp_list_pluck( $flattened, 'answer_id' ) : array(); } } diff --git a/classes/Student.php b/classes/Student.php index 572527869a..509d6f8d95 100644 --- a/classes/Student.php +++ b/classes/Student.php @@ -106,8 +106,8 @@ public function register_student() { return; } - $first_name = sanitize_text_field( tutor_utils()->input_old( 'first_name' ) ); - $last_name = sanitize_text_field( tutor_utils()->input_old( 'last_name' ) ); + $first_name = sanitize_text_field( wp_strip_all_tags( wp_specialchars_decode( tutor_utils()->input_old( 'first_name' ), ENT_QUOTES ) ) ); + $last_name = sanitize_text_field( wp_strip_all_tags( wp_specialchars_decode( tutor_utils()->input_old( 'last_name' ), ENT_QUOTES ) ) ); $email = sanitize_text_field( tutor_utils()->input_old( 'email' ) ); $user_login = sanitize_text_field( tutor_utils()->input_old( 'user_login' ) ); $password = sanitize_text_field( tutor_utils()->input_old( 'password' ) ); @@ -198,14 +198,14 @@ public function update_profile() { $user_id = get_current_user_id(); do_action( 'tutor_profile_update_before', $user_id ); - $first_name = sanitize_text_field( tutor_utils()->input_old( 'first_name' ) ); - $last_name = sanitize_text_field( tutor_utils()->input_old( 'last_name' ) ); + $first_name = sanitize_text_field( wp_strip_all_tags( wp_specialchars_decode( tutor_utils()->input_old( 'first_name' ), ENT_QUOTES ) ) ); + $last_name = sanitize_text_field( wp_strip_all_tags( wp_specialchars_decode( tutor_utils()->input_old( 'last_name' ), ENT_QUOTES ) ) ); $phone_number = sanitize_text_field( tutor_utils()->input_old( 'phone_number' ) ); $tutor_profile_bio = wp_kses( Input::post( 'tutor_profile_bio', '', Input::TYPE_KSES_POST ), tutor_utils()->allowed_profile_bio_tags() ); $tutor_profile_job_title = sanitize_text_field( tutor_utils()->input_old( 'tutor_profile_job_title' ) ); $timezone = Input::post( 'timezone', '' ); - $display_name = sanitize_text_field( tutor_utils()->input_old( 'display_name' ) ); + $display_name = sanitize_text_field( wp_strip_all_tags( wp_specialchars_decode( tutor_utils()->input_old( 'display_name' ), ENT_QUOTES ) ) ); $userdata = array( 'ID' => $user_id,