REPORT / 01
Analysis Report · Folder Analysis cache/learnpress_4.3.2.1 → cache/learnpress_4.3.2.2 — CVE-2025-14802
Shared security patch analysis results
02 ·
Lifecycle actions
cancel · resume · skip · regenerate
03 ·
Share this analysis
copy link · embed report
03 ·
CVE Security Analysis & Writeups
ai-generated · per cve
Comprehensive security analysis generated by AI for each confirmed CVE match. Click on a CVE to view the detailed writeup including vulnerability background, technical details, patch analysis, and PoC guide.
CVE-2025-14802
NVD
AI-Generated Analysis
05 ·
Findings
filter · search · paginate
Showing 0 to 0 of 0 results
inc/Databases/DataBase.php
AI: No vulnerabilities
CVE-2025-14802
--- cache/learnpress_4.3.2.1/inc/Databases/DataBase.php 2026-01-07 00:41:18.609155665 +0000+++ cache/learnpress_4.3.2.2/inc/Databases/DataBase.php 2026-01-07 00:45:50.813787469 +0000@@ -1,890 +1,890 @@-<?php--namespace LearnPress\Databases;--use Exception;-use LearnPress\Filters\FilterBase;-use wpdb;--defined( 'ABSPATH' ) || exit();--/**- * Class DataBase- *- * @since 4.2.9.3- * @version 1.0.0- */-class DataBase {- private static $_instance;- public $wpdb, $tb_users;- public $tb_lp_courses;- public $tb_lp_user_items, $tb_lp_user_itemmeta;- public $tb_posts, $tb_postmeta, $tb_options;- public $tb_terms, $tb_term_relationships, $tb_term_taxonomy;- public $tb_lp_order_items, $tb_lp_order_itemmeta;- public $tb_lp_sections, $tb_lp_section_items;- public $tb_lp_quiz_questions;- public $tb_lp_user_item_results;- public $tb_lp_question_answers;- public $tb_lp_question_answermeta;- public $tb_lp_upgrade_db;- public $tb_lp_sessions;- public $tb_lp_files;- public $tb_thim_cache;- private $collate = '';- public $max_index_length = '191';-- protected function __construct() {- /**- * @var wpdb $wpdb- */- global $wpdb;- $prefix = $wpdb->prefix;-- $this->wpdb = $wpdb;- $this->tb_users = $wpdb->users;- $this->tb_posts = $wpdb->posts;- $this->tb_postmeta = $wpdb->postmeta;- $this->tb_options = $wpdb->options;- $this->tb_terms = $wpdb->terms;- $this->tb_term_relationships = $wpdb->term_relationships;- $this->tb_term_taxonomy = $wpdb->term_taxonomy;- $this->tb_lp_courses = $prefix . 'learnpress_courses';- $this->tb_lp_user_items = $prefix . 'learnpress_user_items';- $this->tb_lp_user_itemmeta = $prefix . 'learnpress_user_itemmeta';- $this->tb_lp_order_items = $prefix . 'learnpress_order_items';- $this->tb_lp_order_itemmeta = $prefix . 'learnpress_order_itemmeta';- $this->tb_lp_section_items = $prefix . 'learnpress_section_items';- $this->tb_lp_sections = $prefix . 'learnpress_sections';- $this->tb_lp_quiz_questions = $prefix . 'learnpress_quiz_questions';- $this->tb_lp_user_item_results = $prefix . 'learnpress_user_item_results';- $this->tb_lp_question_answers = $prefix . 'learnpress_question_answers';- $this->tb_lp_question_answermeta = $prefix . 'learnpress_question_answermeta';- $this->tb_lp_upgrade_db = $prefix . 'learnpress_upgrade_db';- $this->tb_lp_sessions = $prefix . 'learnpress_sessions';- $this->tb_lp_files = $prefix . 'learnpress_files';- $this->tb_thim_cache = $prefix . 'thim_cache';- $this->wpdb->hide_errors();- $this->set_collate();- }-- /**- * Get Instance- *- * @return DataBase- */- public static function getInstance() {- if ( is_null( self::$_instance ) ) {- self::$_instance = new self();- }-- return self::$_instance;- }-- public function set_collate() {- $collate = '';-- if ( $this->wpdb->has_cap( 'collation' ) ) {- if ( ! empty( $this->wpdb->charset ) ) {- $collate .= 'DEFAULT CHARACTER SET ' . $this->wpdb->charset;- }-- if ( ! empty( $this->wpdb->collate ) ) {- $collate .= ' COLLATE ' . $this->wpdb->collate;- }- }-- $this->collate = $collate;- }-- public function get_collate(): string {- return $this->collate;- }-- /**- * Check table exists.- *- * @param string $name_table- *- * @return bool|int- */- public function check_table_exists( string $name_table ) {- return $this->wpdb->query( $this->wpdb->prepare( "SHOW TABLES LIKE '%s'", $name_table ) );- }-- /**- * Clone table- *- * @param string $name_table .- *- * @throws Exception- */- public function clone_table( string $name_table ): bool {- if ( ! current_user_can( ADMIN_ROLE ) ) {- throw new Exception( 'You don\'t have permission' );- }-- $table_bk = $name_table . '_bk';-- // Drop table bk if exists.- $this->drop_table( $table_bk );-- // Clone table- $this->wpdb->query( "CREATE TABLE $table_bk LIKE $name_table" );- $this->wpdb->query( "INSERT INTO $table_bk SELECT * FROM $name_table" );-- /*dbDelta(- "CREATE TABLE $table_bk LIKE $name_table;- INSERT INTO $table_bk SELECT * FROM $name_table;"- );*/-- $this->check_execute_has_error();-- return true;- }-- /**- * Check column table- *- * @param string $name_table .- * @param string $name_col .- *- * @return bool|int- */- public function check_col_table( string $name_table = '', string $name_col = '' ) {- $query = $this->wpdb->prepare( "SHOW COLUMNS FROM $name_table LIKE '%s'", $name_col );-- return $this->wpdb->query( $query );- }-- /**- * Drop Column of Table- *- * @param string $name_table .- * @param string $name_col .- *- * @return bool|int- * @throws Exception- */- public function drop_col_table( string $name_table = '', string $name_col = '' ) {- if ( ! current_user_can( 'administrator' ) ) {- return false;- }-- $check_table = $this->check_col_table( $name_table, $name_col );-- if ( $check_table ) {- $execute = $this->wpdb->query( "ALTER TABLE $name_table DROP COLUMN $name_col" );-- $this->check_execute_has_error();-- return $execute;- }-- return true;- }-- /**- * Add Column of Table- *- * @param string $name_table .- * @param string $name_col .- * @param string $type .- * @param string $after_col .- *- * @return bool|int- * @throws Exception- */- public function add_col_table( string $name_table, string $name_col, string $type, string $after_col = '' ) {- if ( ! current_user_can( ADMIN_ROLE ) ) {- throw new Exception( 'You don\'t have permission' );- }-- $query_add = '';-- $col_exists = $this->check_col_table( $name_table, $name_col );-- if ( ! empty( $after_col ) ) {- $query_add .= "AFTER $after_col";- }-- if ( ! $col_exists ) {- $execute = $this->wpdb->query( "ALTER TABLE $name_table ADD COLUMN $name_col $type $query_add" );-- $this->check_execute_has_error();-- return $execute;- }-- return true;- }-- /**- * Drop Index of Table- *- * @param string $name_table .- *- * @return void- * @throws Exception- */- public function drop_indexs_table( string $name_table ) {- $show_index = "SHOW INDEX FROM $name_table";- $indexs = $this->wpdb->get_results( $show_index );-- foreach ( $indexs as $index ) {- if ( 'PRIMARY' === $index->Key_name || '1' !== $index->Seq_in_index ) {- continue;- }-- $query = "ALTER TABLE $name_table DROP INDEX $index->Key_name";-- $this->wpdb->query( $query );- $this->check_execute_has_error();- }- }-- /**- * Add Index of Table- *- * @param string $name_table .- * @param array $indexs .- *- * @return bool|int- * @throws Exception- */- public function add_indexs_table( string $name_table, array $indexs ) {- $add_index = '';- $count_indexs = count( $indexs ) - 1;-- // Drop indexs .- $this->drop_indexs_table( $name_table );-- foreach ( $indexs as $index ) {- if ( $count_indexs === array_search( $index, $indexs ) ) {- $add_index .= ' ADD INDEX ' . $index . ' (' . $index . ')';- } else {- $add_index .= ' ADD INDEX ' . $index . ' (' . $index . '),';- }- }-- $execute = $this->wpdb->query(- "ALTER TABLE $name_table- $add_index"- );-- $this->check_execute_has_error();-- return $execute;- }-- /**- * Drop table- *- * @param string $name_table .- *- * @return bool|int- * @throws Exception- */- public function drop_table( string $name_table = '' ) {- if ( ! current_user_can( ADMIN_ROLE ) ) {- throw new Exception( 'You don\'t have permission' );- }-- // Check table exists.- $tb_exists = $this->check_table_exists( $name_table );- if ( $tb_exists ) {- $execute = $this->wpdb->query( "DROP TABLE $name_table" );-- $this->check_execute_has_error();-- return $execute;- }-- return true;- }-- /**- * Get list columns name of table- *- * @param string $name_table- *- * @return array- * @throws Exception- * @version 1.0.0- * @since 4.1.6- * @author tungnx- */- public function get_cols_of_table( string $name_table ): array {- $query = "SHOW COLUMNS FROM $name_table";-- $result = $this->wpdb->get_col( $query );-- $this->check_execute_has_error();-- return $result;- }-- /**- * Create table learnpress_user_item_results- *- * @return bool|int- * @throws Exception- */- public function create_tb_lp_user_item_results() {- $collate = $this->get_collate();-- $execute = $this->wpdb->query(- "- CREATE TABLE IF NOT EXISTS $this->tb_lp_user_item_results(- id bigint(20) unsigned NOT NULL AUTO_INCREMENT,- user_item_id bigint(20) unsigned NOT NULL,- result longtext,- PRIMARY KEY (id),- KEY user_item_id (user_item_id)- ) $collate- "- );-- $this->check_execute_has_error();-- return $execute;- }-- /**- * Create table learnpress_upgrade_db- *- * @return bool|int- * @throws Exception- */- public function create_tb_lp_upgrade_db() {- $collate = $this->get_collate();-- $execute = $this->wpdb->query(- "- CREATE TABLE IF NOT EXISTS {$this->tb_lp_upgrade_db}(- step varchar(50) PRIMARY KEY UNIQUE,- status varchar(10),- KEY status (status)- ) $collate- "- );-- $this->check_execute_has_error();-- return $execute;- }-- /**- * Set step completed.- *- * @param string $step .- * @param string $status .- *- * @return int|bool- */- public function set_step_complete( string $step, string $status ) {- if ( ! current_user_can( 'administrator' ) ) {- return false;- }-- return $this->wpdb->insert(- $this->tb_lp_upgrade_db,- array(- 'step' => $step,- 'status' => $status,- ),- array( '%s', '%s' )- );- }-- /**- * Get steps completed.- *- * @return array|object|null- */- public function get_steps_completed() {- return $this->wpdb->get_results( "SELECT step, status FROM {$this->tb_lp_upgrade_db}", OBJECT_K );- }-- /**- * Check execute current has any errors.- *- * @throws Exception- */- public function check_execute_has_error() {- if ( $this->wpdb->last_error ) {- throw new Exception( $this->wpdb->last_error );- }- }-- /**- * Important: Reason need set again indexes for table options of WP- * because if want change value of "option_name" will error "database error Duplicate entry"- * So before set must drop and add when done all- *- * @throws Exception- * @version 1.0.0- * @since 4.0.3- * @author tungnx- */- public function create_indexes_tb_options() {- $this->drop_indexs_table( $this->tb_options );- $result = $this->wpdb->query(- "- ALTER TABLE $this->tb_options- ADD UNIQUE option_name (option_name),- ADD INDEX autoload (autoload)- "- );-- $this->check_execute_has_error();-- return $result;- }-- /**- * Rename table- *- * @throws Exception- * @version 1.0.0- * @since 4.0.3- * @author tungnx- */- public function rename_table( string $name_table = '', string $new_name = '' ) {- if ( ! current_user_can( ADMIN_ROLE ) ) {- throw new Exception( 'You don\'t have permission' );- }-- $tb_exists = $this->check_table_exists( $name_table );-- if ( ! $tb_exists ) {- throw new Exception( 'Table not exists' );- }-- $result = $this->wpdb->query(- "- ALTER TABLE $name_table- RENAME $new_name- "- );- $this->check_execute_has_error();-- return $result;- }-- /**- * Check key postmeta exist on Database- *- * @param int $post_id- * @param string $key- *- * @return bool|int- */- public function check_key_postmeta_exists( int $post_id = 0, string $key = '' ) {- return $this->wpdb->query(- $this->wpdb->prepare(- "- SELECT meta_id FROM $this->tb_postmeta- WHERE meta_key = %s- AND post_id = %d- ",- $key,- $post_id- )- );- }-- /**- * Get total pages- *- * @param int $limit- * @param int $total_rows- *- * @return int- */- public static function get_total_pages( int $limit = 0, int $total_rows = 0 ): int {- if ( $limit == 0 ) {- return 0;- }-- $total_pages = (int) ceil( $total_rows / $limit );-- return (int) $total_pages;- }-- /**- * Get query string single row- *- * @param FilterBase $filter- *- * @since 4.2.5- * @version 1.0.1- */- public function get_query_single_row( &$filter ) {- $filter->limit = 1;- $filter->return_string_query = true;- $filter->run_query_count = false;- }-- /**- * Get result query- *- * @param FilterBase $filter- * @param int $total_rows- *- * @return array|object|null|int|string- * @throws Exception- * @author tungnx- * @version 1.0.2- * @since 4.1.6- */- public function execute( $filter, int &$total_rows = 0 ) {- $result = null;-- // Where- $WHERE = array( 'WHERE 1=1' );-- // Fields select- $FIELDS = '*';- if ( ! empty( $filter->only_fields ) ) {- $FIELDS = implode( ',', array_unique( $filter->only_fields ) );- } elseif ( ! empty( $filter->fields ) ) {- // exclude more fields- if ( ! empty( $filter->exclude_fields ) ) {- foreach ( $filter->exclude_fields as $field ) {- $index_field = array_search( $field, $filter->fields );- if ( $index_field ) {- unset( $filter->fields[ $index_field ] );- }- }- }-- foreach ( $filter->fields as $key => $field ) {- if ( $field === 'order' ) {- // Replace order with `order` to avoid conflict with SQL reserved word.- $filter->fields[ $key ] = '`order`';- break;- }- }-- $FIELDS = implode( ',', array_unique( $filter->fields ) );- }- $FIELDS = apply_filters( 'lp/query/fields', $FIELDS, $filter );-- $INNER_JOIN = array();- $INNER_JOIN = array_merge( $INNER_JOIN, $filter->join );- $INNER_JOIN = apply_filters( 'lp/query/inner_join', $INNER_JOIN, $filter );- $INNER_JOIN = implode( ' ', array_unique( $INNER_JOIN ) );-- $WHERE = array_merge( $WHERE, $filter->where );- $WHERE = apply_filters( 'lp/query/where', $WHERE, $filter );- $WHERE = implode( ' ', array_unique( $WHERE ) );-- // Group by- $GROUP_BY = '';- if ( $filter->group_by ) {- $GROUP_BY .= 'GROUP BY ' . $filter->group_by;- $GROUP_BY = apply_filters( 'lp/query/group_by', $GROUP_BY, $filter );- }-- // Order by- $ORDER_BY = '';- if ( $filter->order_by ) {- $filter->order = strtoupper( $filter->order );- if ( ! in_array( $filter->order, [ 'DESC', 'ASC' ] ) ) {- $filter->order = 'DESC';- }-- $ORDER_BY .= 'ORDER BY ' . $filter->order_by . ' ' . $filter->order . ' ';- $ORDER_BY = apply_filters( 'lp/query/order_by', $ORDER_BY, $filter );- }-- // Limit- $LIMIT = '';- if ( $filter->limit != - 1 ) {- $filter->limit = absint( $filter->limit );- /*if ( $filter->limit > $filter->max_limit ) {- $filter->limit = $filter->max_limit;- }*/- $offset = $filter->limit * ( $filter->page - 1 );- $LIMIT = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $filter->limit );- }-- // For nest query- if ( $filter->return_string_query ) {- $LIMIT = '';- }-- // From table or group select- $COLLECTION = '';- if ( ! empty( $filter->collection ) ) {- $COLLECTION = $filter->collection;- }-- // Alias table- $ALIAS_COLLECTION = 'X';- if ( ! empty( $filter->collection_alias ) ) {- $ALIAS_COLLECTION = $filter->collection_alias;- }-- // Query- $query = "SELECT $FIELDS FROM $COLLECTION AS $ALIAS_COLLECTION- $INNER_JOIN- $WHERE- $GROUP_BY- $ORDER_BY- $LIMIT- ";-- if ( $filter->return_string_query ) {- return $query;- } elseif ( ! empty( $filter->union ) ) {- $query = implode( ' UNION ', array_unique( $filter->union ) );- $query .= $GROUP_BY;- $query .= $ORDER_BY;- $query .= $LIMIT;- }-- if ( ! $filter->query_count ) {- // Debug string query- if ( $filter->debug_string_query ) {- return $query;- }-- $result = $this->wpdb->get_results( $query );- }-- // Query total rows- if ( $filter->run_query_count ) {- $query = str_replace( array( $LIMIT, $ORDER_BY ), '', $query );- $query_total = "SELECT COUNT($filter->field_count) FROM ($query) AS $ALIAS_COLLECTION";- $total_rows = (int) $this->wpdb->get_var( $query_total );-- $this->check_execute_has_error();-- if ( $filter->query_count ) {- // Debug string query- if ( $filter->debug_string_query ) {- return $query_total;- }-- return $total_rows;- }- }-- $this->check_execute_has_error();-- return $result;- }-- /**- * Query update- *- * @param FilterBase $filter- *- * @throws Exception- * @since 4.1.7- * @version 1.0.1- */- public function update_execute( $filter ) {-- $COLLECTION = $filter->collection;-- // SET value- $SET = apply_filters( 'lp/query/update/set', $filter->set, $filter );- $SET = implode( ',', array_unique( $SET ) );-- // Where- $WHERE = array( 'WHERE 1=1' );- $WHERE = array_merge( $WHERE, $filter->where );- $WHERE = apply_filters( 'lp/query/update/where', $WHERE, $filter );- $WHERE = implode( ' ', array_unique( $WHERE ) );-- $query = "- UPDATE $COLLECTION- SET $SET- $WHERE- ";-- $result = $this->wpdb->query( $query );-- $this->check_execute_has_error();-- return $result;- }-- /**- * Query delete- *- * @param FilterBase $filter- * @param string $table- *- * @return bool|int|\mysqli_result|string|null- * @throws Exception- * @since 4.1.7- * @version 1.0.1- */- public function delete_execute( $filter, string $table = '' ) {- $COLLECTION = $filter->collection;-- // Where- $WHERE = array( 'WHERE 1=1' );- $WHERE = array_merge( $WHERE, $filter->where );- $WHERE = apply_filters( 'lp/query/delete/where', $WHERE, $filter );- $WHERE = implode( ' ', array_unique( $WHERE ) );-- // Join- $INNER_JOIN = array();- $INNER_JOIN = array_merge( $INNER_JOIN, $filter->join );- $INNER_JOIN = apply_filters( 'lp/query/delete/inner_join', $INNER_JOIN, $filter );- $INNER_JOIN = implode( ' ', array_unique( $INNER_JOIN ) );-- $query = "- DELETE $table FROM $COLLECTION- $INNER_JOIN- $WHERE- ";-- if ( $filter->return_string_query ) {- return $query;- }-- $result = $this->wpdb->query( $query );-- $this->check_execute_has_error();-- return $result;- }-- /**- * Get values of list object by key- *- * @param array $arr_object- * @param string $key- *- * @return array- */- public static function get_values_by_key( array $arr_object, string $key = 'ID' ): array {- $arr_object_ids = array();- foreach ( $arr_object as $object ) {- $arr_object_ids[] = $object->{$key};- }-- return $arr_object_ids;- }-- /**- * Insert data- *- * @param array $args [ 'data' => [], 'filter' => FilterBase, 'table_name' => '', 'key_auto_increment' => '' ]- *- * @return int- * @throws Exception- * @version 1.0.1- * @since 4.2.9- */- public function insert_data( array $args ): int {- $data = $args['data'] ?? [];- $filter = $args['filter'] ?? null;- $table_name = $args['table_name'] ?? '';- $key_auto_increment = $args['key_auto_increment'] ?? '';- $key_auto_increment = sanitize_key( $key_auto_increment );-- if ( empty( $data ) || ! is_array( $data ) ) {- throw new Exception( __( 'Data must be an array!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }-- /*if ( ! $filter instanceof FilterBase ) {- throw new Exception( __( 'Invalid filter!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }*/-- if ( empty( $filter->all_fields ) ) {- throw new Exception( __( 'Filter must have property all_fields!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }-- if ( empty( $table_name ) ) {- throw new Exception( __( 'Table name is required!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }-- if ( empty( $key_auto_increment ) || ! is_string( $key_auto_increment ) ) {- throw new Exception( __( 'Key auto increment must be a string!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }-- foreach ( $data as $col_name => $value ) {- if ( ! in_array( $col_name, $filter->all_fields ) ) {- unset( $data[ $col_name ] );- }- }-- // unset key is auto increment.- unset( $data[ $key_auto_increment ] );-- $this->wpdb->insert( $table_name, $data );-- $this->check_execute_has_error();-- return $this->wpdb->insert_id;- }-- /**- * Update data- *- * @param array $args- *- * @return bool- *- * @throws Exception- * @since 4.2.9- * @version 1.0.1- */- public function update_data( array $args ): bool {- $data = $args['data'] ?? [];- $filter = $args['filter'] ?? null;- $table_name = $args['table_name'] ?? '';- $where_key = $args['where_key'] ?? '';- $where_key = sanitize_key( $where_key );-- /*if ( ! $filter instanceof FilterBase ) {- throw new Exception( __( 'Invalid filter!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }*/-- if ( empty( $filter->all_fields ) ) {- throw new Exception( __( 'Filter must have property all_fields!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }-- if ( empty( $data ) || ! is_array( $data ) ) {- throw new Exception( __( 'Data must be an array!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }-- if ( empty( $where_key ) ) {- throw new Exception( __( 'Invalid where key!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }-- if ( empty( $table_name ) ) {- throw new Exception( __( 'Table name is required!', 'learnpress' ) . ' | ' . __FUNCTION__ );- }-- $filter->collection = $table_name;- foreach ( $data as $col_name => $value ) {- if ( ! in_array( $col_name, $filter->all_fields ) ) {- continue;- }-- // Key `order` is reserved keyword in MySQL- if ( $col_name === 'order' ) {- $col_name = '`order`';- }-- if ( is_null( $value ) ) {- $filter->set[] = $col_name . ' = null';- } else {- $filter->set[] = $this->wpdb->prepare( $col_name . ' = %s', $value );- }- }-- $filter->where[] = $this->wpdb->prepare( "AND $where_key = %d", $data[ $where_key ] );- $this->update_execute( $filter );-- return true;- }-}+<?php + +namespace LearnPress\Databases; + +use Exception; +use LearnPress\Filters\FilterBase; +use wpdb; + +defined( 'ABSPATH' ) || exit(); + +/** + * Class DataBase + * + * @since 4.2.9.3 + * @version 1.0.0 + */ +class DataBase { + private static $_instance; + public $wpdb, $tb_users; + public $tb_lp_courses; + public $tb_lp_user_items, $tb_lp_user_itemmeta; + public $tb_posts, $tb_postmeta, $tb_options; + public $tb_terms, $tb_term_relationships, $tb_term_taxonomy; + public $tb_lp_order_items, $tb_lp_order_itemmeta; + public $tb_lp_sections, $tb_lp_section_items; + public $tb_lp_quiz_questions; + public $tb_lp_user_item_results; + public $tb_lp_question_answers; + public $tb_lp_question_answermeta; + public $tb_lp_upgrade_db; + public $tb_lp_sessions; + public $tb_lp_files; + public $tb_thim_cache; + private $collate = ''; + public $max_index_length = '191'; + + protected function __construct() { + /** + * @var wpdb $wpdb + */ + global $wpdb; + $prefix = $wpdb->prefix; + + $this->wpdb = $wpdb; + $this->tb_users = $wpdb->users; + $this->tb_posts = $wpdb->posts; + $this->tb_postmeta = $wpdb->postmeta; + $this->tb_options = $wpdb->options; + $this->tb_terms = $wpdb->terms; + $this->tb_term_relationships = $wpdb->term_relationships; + $this->tb_term_taxonomy = $wpdb->term_taxonomy; + $this->tb_lp_courses = $prefix . 'learnpress_courses'; + $this->tb_lp_user_items = $prefix . 'learnpress_user_items'; + $this->tb_lp_user_itemmeta = $prefix . 'learnpress_user_itemmeta'; + $this->tb_lp_order_items = $prefix . 'learnpress_order_items'; + $this->tb_lp_order_itemmeta = $prefix . 'learnpress_order_itemmeta'; + $this->tb_lp_section_items = $prefix . 'learnpress_section_items'; + $this->tb_lp_sections = $prefix . 'learnpress_sections'; + $this->tb_lp_quiz_questions = $prefix . 'learnpress_quiz_questions'; + $this->tb_lp_user_item_results = $prefix . 'learnpress_user_item_results'; + $this->tb_lp_question_answers = $prefix . 'learnpress_question_answers'; + $this->tb_lp_question_answermeta = $prefix . 'learnpress_question_answermeta'; + $this->tb_lp_upgrade_db = $prefix . 'learnpress_upgrade_db'; + $this->tb_lp_sessions = $prefix . 'learnpress_sessions'; + $this->tb_lp_files = $prefix . 'learnpress_files'; + $this->tb_thim_cache = $prefix . 'thim_cache'; + $this->wpdb->hide_errors(); + $this->set_collate(); + } + + /** + * Get Instance + * + * @return DataBase + */ + public static function getInstance() { + if ( is_null( self::$_instance ) ) { + self::$_instance = new self(); + } + + return self::$_instance; + } + + public function set_collate() { + $collate = ''; + + if ( $this->wpdb->has_cap( 'collation' ) ) { + if ( ! empty( $this->wpdb->charset ) ) { + $collate .= 'DEFAULT CHARACTER SET ' . $this->wpdb->charset; + } + + if ( ! empty( $this->wpdb->collate ) ) { + $collate .= ' COLLATE ' . $this->wpdb->collate; + } + } + + $this->collate = $collate; + } + + public function get_collate(): string { + return $this->collate; + } + + /** + * Check table exists. + * + * @param string $name_table
AI Analysis
I'll analyze this diff for security vulnerabilities. This appears to be a line-ending change (CRLF to LF conversion) rather than actual code modifications. Let me examine the content carefully. The diff shows the entire file being replaced, but comparing the old and new versions, the actual code logic is identical - only the line endings have changed (from Windows-style to Unix-style line endings). **Analysis Results:** Vulnerability Existed: **no** Explanation: This diff represents only a line-ending conversion (CRLF to LF) with no actual code logic changes. The file content remains functionally identical between versions 4.3.2.1 and 4.3.2.2. Since no code has been modified, no new vulnerabilities have been introduced, and no existing vulnerabilities have been fixed. The security posture of the code is unchanged by this modification. The file continues to have the same pre-existing SQL injection risks in various methods (such as `clone_table`, `drop_col_table`, `add_col_table`, etc.) where table and column names are directly interpolated into SQL queries without proper escaping, but these are not addressed by this update.
CVE Analysis Results:
CVE-2025-14802: Yes
View CVE Description
The LearnPress – WordPress LMS Plugin for WordPress is vulnerable to unauthorized file deletion in versions up to, and including, 4.3.2.2 via the /wp-json/lp/v1/material/{file_id} REST API endpoint. This is due to a parameter mismatch between the DELETE operation and authorization check, where the endpoint uses file_id from the URL path but the permission callback validates item_id from the request body. This makes it possible for authenticated attackers, with teacher-level access, to delete arbitrary lesson material files uploaded by other teachers via sending a DELETE request with their own item_id (to pass authorization) while targeting another teacher's file_id.
inc/Databases/class-lp-statistics-db.php
AI: No vulnerabilities
CVE-2025-14802
--- cache/learnpress_4.3.2.1/inc/Databases/class-lp-statistics-db.php 2026-01-07 00:41:18.593154690 +0000+++ cache/learnpress_4.3.2.2/inc/Databases/class-lp-statistics-db.php 2026-01-07 00:45:50.789785998 +0000@@ -1,818 +1,819 @@-<?php-/**- * Class LP_Statistics_DB- *- * @author thimpress- * @since 4.2.6- */--defined( 'ABSPATH' ) || exit();--class LP_Statistics_DB extends LP_Database {- private static $_instance;-- protected function __construct() {- parent::__construct();- }-- public static function getInstance() {- if ( is_null( self::$_instance ) ) {- self::$_instance = new self();- }-- return self::$_instance;- }-- /**- * filter to get data for chart of a day.- * @param LP_Filter $filter- * @param string $time_field the column use to filter time- * @return LP_Filter- */- public function chart_filter_date_group_by( LP_Filter $filter, string $time_field ) {- $filter->only_fields[] = "HOUR($time_field) as x_data_label";- $filter->group_by = 'x_data_label';- return $filter;- }- /**- * filter to get data for chart of last some days. ex: last 7 days, last 30 days,...- * @param LP_Filter $filter- * @param string $time_field the column use to filter time- * @return LP_Filter- */- public function chart_filter_previous_days_group_by( LP_Filter $filter, string $time_field ) {- $filter->only_fields[] = "CAST($time_field AS DATE) as x_data_label";- $filter->group_by = 'x_data_label';- return $filter;- }- /**- * filter to get data for chart of a month- * @param LP_Filter $filter- * @param string $time_field the column use to filter time- * @return LP_Filter- */- public function chart_filter_month_group_by( LP_Filter $filter, string $time_field ) {- $filter->only_fields[] = "DAY($time_field) as x_data_label";- $filter->group_by = 'x_data_label';- return $filter;- }- /**- * filter to get data for chart of months. ex: last 3 months, 6 months, 9 months,...- * @param LP_Filter $filter- * @param string $time_field the column use to filter time- * @return LP_Filter- */- public function chart_filter_previous_months_group_by( LP_Filter $filter, string $time_field ) {- $filter->only_fields[] = "DATE_FORMAT( $time_field , '%m-%Y') as x_data_label";- $filter->group_by = 'x_data_label';- return $filter;- }- /**- * filter to get data for chart of a year- * @param LP_Filter $filter- * @param string $time_field the column use to filter time. ex: post_date with posts table, user_registered on users table- * @return LP_Filter- */- public function chart_filter_year_group_by( LP_Filter $filter, string $time_field ) {- $filter->only_fields[] = "MONTH($time_field) as x_data_label";- $filter->group_by = 'x_data_label';- return $filter;- }- /**- * filter to get data for chart of a custom date ranges- * @param LP_Filter $filter- * @param array $dates array of date range use to filer- * @param string $time_field the column use to filter time. ex: post_date with posts table, user_registered on users table- * @return LP_Filter- */- public function chart_filter_custom_group_by( LP_Filter $filter, array $dates, string $time_field ) {- $diff1 = date_create( $dates[0] );- $diff2 = date_create( $dates[1] );- if ( ! $diff1 || ! $diff2 ) {- throw new Exception( 'Custom filter date is invalid.', 'learnpress' );- }- $diff = date_diff( $diff1, $diff2, true );- $y = $diff->y;- $m = $diff->m;- $d = $diff->d;- if ( $y < 1 ) {- if ( $m <= 1 ) {- if ( $d < 1 ) {- $filter = $this->chart_filter_date_group_by( $filter, $time_field );- } else {- // more thans 2 days return data of days- $filter = $this->chart_filter_previous_days_group_by( $filter, $time_field );- }- } else {- // more thans 2 months return data of months- $filter = $this->chart_filter_previous_months_group_by( $filter, $time_field );- }- } elseif ( $y < 2 ) {- // less thans 2 years return data of year months- $filter = $this->chart_filter_previous_months_group_by( $filter, $time_field );- } elseif ( $y < 5 ) {- // from 2-5years return data of year quarters- $filter->only_fields[] = $this->wpdb->prepare( "CONCAT( %s, QUARTER($time_field) ,%s, Year($time_field)) as x_data_label", [ 'q', '-' ] );- $filter->group_by = 'x_data_label';- } else {- // more than 5 years, return data of years- $filter->only_fields[] = "YEAR($time_field) as x_data_label";- $filter->group_by = 'x_data_label';- }- return $filter;- }- /**- * @param LP_Filter $filter- * @param string $date choose a date to query, format Y-m-d- * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on- * @return LP_Filter- */- public function date_filter( LP_Filter $filter, string $date, string $time_field, $is_until = false ) {- if ( $is_until ) {- $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)<= cast(%s as DATE)", $date );- } else {- $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)= cast(%s as DATE)", $date );- }- return $filter;- }- /**- * @param LP_Filter $filter- * @param int $value ex: 7 - last 7 days, 10 - last 10 days, ...- * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on- * @return LP_Filter- */- public function previous_days_filter( LP_Filter $filter, int $value, string $time_field, $is_until = false ) {- if ( $value < 2 ) {- throw new Exception( 'Day must be greater than 2 days.', 'learnpress' );- }- if ( $is_until ) {- $filter->where[] = $this->wpdb->prepare( "AND $time_field <= CURDATE()" );- } else {- $filter->where[] = $this->wpdb->prepare( "AND $time_field >= DATE_ADD(CURDATE(), INTERVAL -%d DAY)", $value );- }-- return $filter;- }- /**- * @param LP_Filter $filter- * @param string $date choose a date to query, format Y-m-d- * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on- * @return LP_Filter- */- public function month_filter( LP_Filter $filter, string $date, string $time_field, $is_until = false ) {- if ( $is_until ) {- $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)<= cast(%s as DATE)", $date );- } else {- $filter->where[] = $this->wpdb->prepare( "AND EXTRACT(YEAR_MONTH FROM $time_field)= EXTRACT(YEAR_MONTH FROM %s)", $date );- }- return $filter;- }- /**- * @param LP_Filter $filter- * @param int $value ex: 3 - last 3 months, 10 - last 10 months, ...- * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on- * @return LP_Filter- */- public function previous_months_filter( LP_Filter $filter, int $value, string $time_field, $is_until = false ) {- if ( $value < 2 ) {- throw new Exception( 'Values must be greater than 2 months.', 'learnpress' );- }- if ( $is_until ) {- $filter->where[] = $this->wpdb->prepare( "AND $time_field <= CURDATE()" );- } else {- $filter->where[] = $this->wpdb->prepare( "AND EXTRACT(YEAR_MONTH FROM $time_field) >= EXTRACT(YEAR_MONTH FROM DATE_ADD(CURDATE(), INTERVAL -%d MONTH))", $value );- }- return $filter;- }- /**- * get data for each month in year- * @param LP_Filter $filter- * @param string $date choose a date to query, format Y-m-d- * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on- * @return LP_Filter- */- public function year_filter( LP_Filter $filter, string $date, string $time_field, $is_until = false ) {- if ( $is_until ) {- $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE) <= cast(%s as DATE)", $date );- } else {- $filter->where[] = $this->wpdb->prepare( "AND YEAR($time_field)= YEAR(%s)", $date );- }- return $filter;- }- /**- * custom query with data range- * @param LP_Filter $filter- * @param array $dates date ranges, array of 2 dates.- * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on- * @return LP_Filter- */- public function custom_time_filter( LP_Filter $filter, array $dates, string $time_field, $is_until = false ) {- if ( empty( $dates ) ) {- throw new Exception( 'Select date', 'learnpress' );- }- sort( $dates );- if ( $is_until ) {- $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE) <= cast(%s as DATE)", $dates[1] );- } else {- $filter->where[] = $this->wpdb->prepare(- "AND (DATE($time_field) BETWEEN %s AND %s)",- date( 'Y-m-d', strtotime( $dates[0] ) ),- date( 'Y-m-d', strtotime( $dates[1] ) )- );- }-- return $filter;- }-- /**- * choose filter type foreach filter time- * @param LP_Filter $filter- * @param string $type date|month|year|previous_days|custom- * @param string $time_field datetime colummn- * @param boolean $value value to query datetimes- * @param boolean $is_until filter time by the last date- * @return LP_Filter- */- public function filter_time( LP_Filter $filter, string $type, string $time_field, $value = false, $is_until = false ) {- if ( ! $value ) {- throw new Exception( 'Empty statistic time', 'learnpress' );- }- switch ( $type ) {- case 'date':- $filter = $this->date_filter( $filter, $value, $time_field, $is_until );- break;- case 'month':- $filter = $this->month_filter( $filter, $value, $time_field, $is_until );- break;- case 'year':- $filter = $this->year_filter( $filter, $value, $time_field, $is_until );- break;- case 'previous_days':- $filter = $this->previous_days_filter( $filter, (int) $value, $time_field, $is_until );- break;- case 'previous_months':- $filter = $this->previous_months_filter( $filter, (int) $value, $time_field, $is_until );- break;- case 'custom':- $value = explode( '+', $value );- if ( count( $value ) !== 2 ) {- throw new Exception( 'Invalid custom time', 'learnpress' );- }- $filter = $this->custom_time_filter( $filter, $value, $time_field, $is_until );- default:- // code...- break;- }- return $filter;- }- /**- * format return data foreach type of filter- * @param LP_Filter $filter- * @param string $type date|month|year|previous_days|custom- * @param string $time_field datetime colummn- * @param boolean $value value to query datetimes- * @return LP_Filter- */- public function chart_filter_group_by( LP_Filter $filter, string $type, string $time_field, $value = false ) {- switch ( $type ) {- case 'date':- $filter = $this->chart_filter_date_group_by( $filter, $time_field );- break;- case 'month':- $filter = $this->chart_filter_month_group_by( $filter, $time_field );- break;- case 'year':- $filter = $this->chart_filter_year_group_by( $filter, $time_field );- break;- case 'previous_days':- $filter = $this->chart_filter_previous_days_group_by( $filter, $time_field );- break;- case 'previous_months':- $filter = $this->chart_filter_previous_months_group_by( $filter, $time_field );- break;- case 'custom':- if ( empty( $value ) ) {- throw new Exception( 'Empty statistic time', 'learnpress' );- }- $value = explode( '+', $value );- if ( count( $value ) !== 2 ) {- throw new Exception( 'Invalid custom time', 'learnpress' );- }- $filter = $this->chart_filter_custom_group_by( $filter, $value, $time_field );- default:- // code...- break;- }- return $filter;- }-- /**- * get_completed_order_data use this for complete order report chart- * @param string $type time type filter: date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- * @return array completed order data- */- public function get_completed_order_data( string $type, string $value ) {- if ( ! $type || ! $value ) {- return [];- }- $filter = new LP_Order_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $time_field = 'p.post_date';-- // count completed orders- $filter->only_fields[] = 'count( p.ID) as x_data';- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value );- $filter->where[] = $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB );- $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type );- $filter->limit = -1;- $filter->order_by = $time_field;- $filter->order = 'asc';-- $filter->run_query_count = false;- $result = $this->execute( $filter );-- return $result;- }-- /**- * query to count LP Orders with all statuses- * @param LP_Order_Filter $filter- * @return LP_Order_Filter- */- public function filter_order_count_statics( LP_Order_Filter $filter ) {- // $filter->query_count = true;- $filter->only_fields[] = 'count( p.ID) as count_order';- $filter->only_fields[] = 'REPLACE(p.post_status,"lp-","") as order_status';- $filter->group_by = 'p.post_status';- $filter->where[] = $this->wpdb->prepare( 'AND p.post_status LIKE CONCAT(%s,"%")', 'lp-' );- $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type );- $filter->run_query_count = false;-- return $filter;- }- /**- * get LP Order count of a filter time- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- * @return array result of LP Order count foreach status- */- public function get_order_statics( string $type, string $value ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Order_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $time_field = 'p.post_date';- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter = $this->filter_order_count_statics( $filter );- $filter->limit = -1;- $result = $this->execute( $filter );-- return $result;- }- /*Overviews statistics*/- /**- * get sales amount of complete order- * @param string $type [time type filter: date|month|year|previous_days|custom]- * @param string $value [time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom ]- * @return array completed order data- */- public function get_net_sales_data( string $type, string $value ) {- if ( ! $type || ! $value ) {- return [];- }- $filter = new LP_Order_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $oi_table = $this->tb_lp_order_items;- $oim_table = $this->tb_lp_order_itemmeta;- // net sales summary- $filter->only_fields[] = 'SUM(oim.meta_value) as x_data';- $time_field = 'p.post_date';- $filter->join = [- "INNER JOIN $oi_table AS oi ON p.ID = oi.order_id",- "INNER JOIN $oim_table AS oim ON oi.order_item_id = oim.learnpress_order_item_id",- ];- $filter->limit = -1;- $filter->where = [- $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type ),- $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB ),- $this->wpdb->prepare( 'AND oim.meta_key=%s', '_total' ),- ];- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value );- $filter->order_by = $time_field;- $filter->order = 'asc';- $filter->run_query_count = false;- - $result = $this->execute( $filter );- // error_log( $this->check_execute_has_error() );- return $result;- }-- /**- * get top categories of sold course- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- * @param integer $limit limit of query, default is 10- * @param boolean $exclude_free_course exclude free course- * @return array return term_id and term_count- */- public function get_top_sold_categories( string $type, string $value, $limit = 0, $exclude_free_course = false ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Order_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $filter->only_fields[] = 'r_term.term_taxonomy_id as term_id';- $filter->only_fields[] = 'COUNT(r_term.term_taxonomy_id) as term_count';- $filter->only_fields[] = 'terms.name as term_name';- $filter->limit = $limit > 0 ? $limit : 10;- $time_field = 'p.post_date';- $tb_term_relationships = $this->tb_term_relationships;- $tb_term_taxonomy = $this->tb_term_taxonomy;- $tb_terms = $this->tb_terms;- $oi_table = $this->tb_lp_order_items;- $oim_table = $this->tb_lp_order_itemmeta;-- $filter->join = [- "INNER JOIN $oi_table AS oi ON p.ID = oi.order_id",- "INNER JOIN $oim_table AS oim ON oi.order_item_id = oim.learnpress_order_item_id",- "INNER JOIN $tb_term_relationships AS r_term ON oi.item_id = r_term.object_id",- "INNER JOIN $tb_term_taxonomy AS tax_term ON tax_term.term_taxonomy_id = r_term.term_taxonomy_id",- "INNER JOIN $tb_terms AS terms ON terms.term_id = r_term.term_taxonomy_id",- ];-- $filter->where = array(- $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type ),- $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB ),- $this->wpdb->prepare( 'AND oi.item_type=%s', LP_COURSE_CPT ),- $this->wpdb->prepare( 'AND tax_term.taxonomy=%s', LP_COURSE_CATEGORY_TAX ),- );- $filter = $this->filter_time( $filter, $type, $time_field, $value );- if ( $exclude_free_course ) {- $filter->where[] = $this->wpdb->prepare( 'AND oim.meta_key=%s', '_total' );- $filter->where[] = $this->wpdb->prepare( 'AND oim.meta_value > 0' );- }- $filter->group_by = 'term_id';- $filter->order_by = 'term_count';- $filter->order = 'DESC';- $filter->run_query_count = false;- $result = $this->execute( $filter );-- return $result;- }-- /**- * get top courses was sold in the filter- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- * @param integer $limit limit of query, default 10- * @param boolean $exclude_free_course exclude free course, get result only purchase course- * @return array $result- */- public function get_top_sold_courses( string $type, string $value, $limit = 0, $exclude_free_course = false ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Order_Filter();- $tb_posts = $this->tb_posts;- $filter->collection = $tb_posts;- $filter->collection_alias = 'p';- $filter->only_fields[] = 'oi.item_id as course_id';- $filter->only_fields[] = 'COUNT(oi.item_id) as course_count';- $filter->only_fields[] = 'p2.post_title as course_name';- $filter->limit = $limit > 0 ? $limit : 10;- $time_field = 'p.post_date';- $oi_table = $this->tb_lp_order_items;- $oim_table = $this->tb_lp_order_itemmeta;-- $filter->join = [- "INNER JOIN $oi_table AS oi ON p.ID = oi.order_id",- "INNER JOIN $oim_table AS oim ON oi.order_item_id = oim.learnpress_order_item_id",- "INNER JOIN $tb_posts AS p2 ON p2.ID = oi.item_id",- ];- $filter->where = array(- $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type ),- $this->wpdb->prepare( 'AND p.post_status=%s', LP_ORDER_COMPLETED_DB ),- $this->wpdb->prepare( 'AND oi.item_type=%s', LP_COURSE_CPT ),- );- $filter = $this->filter_time( $filter, $type, $time_field, $value );- if ( $exclude_free_course ) {- $filter->where[] = $this->wpdb->prepare( 'AND oim.meta_key=%s', '_total' );- $filter->where[] = $this->wpdb->prepare( 'AND oim.meta_value > 0' );- }- $filter->group_by = 'course_id';- $filter->order_by = 'course_count';- $filter->order = 'DESC';- $filter->run_query_count = false;- $result = $this->execute( $filter );-- return $result;- }- /**- * Overviews get total courses was created ( all statuses )- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string- * @return int $result course count- */- public function get_total_course_created( string $type, string $value ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Course_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $filter->only_fields[] = 'p.ID';- $time_field = 'p.post_date';-- $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', LP_COURSE_CPT );- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter->query_count = true;- $result = $this->execute( $filter );- return $result;- }- /**- * Overviews get total orders was created ( all statuses )- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string- * @return int $result order count- */- public function get_total_order_created( string $type, string $value ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Course_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $filter->only_fields[] = 'p.ID';- $time_field = 'p.post_date';-- $filter->where[] = $this->wpdb->prepare( 'AND p.post_type = %s', LP_ORDER_CPT );- $filter->where[] = $this->wpdb->prepare( 'AND p.post_status != %s', 'auto-draft' );- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter->query_count = true;- $result = $this->execute( $filter );- return $result;- }- /**- * Overviews get total instructors was created ( administrator and lp_teacher )- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string- * @return int $result user count- */- public function get_total_instructor_created( string $type, string $value ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Filter();- $filter->collection = $this->wpdb->users;- $filter->collection_alias = 'u';- $filter->only_fields[] = 'u.ID';- $usermeta_table = $this->wpdb->usermeta;- $filter->join[] = "INNER JOIN $usermeta_table AS um ON um.user_id = u.ID";- $time_field = 'u.user_registered';- $filter->where[] = $this->wpdb->prepare( 'AND um.meta_key=%s', $this->wpdb->prefix . 'capabilities' );- $filter->where[] = $this->wpdb->prepare( "AND um.meta_value LIKE CONCAT('%',%s,'%')", ADMIN_ROLE );- $filter->where[] = $this->wpdb->prepare( "OR um.meta_value LIKE CONCAT('%',%s,'%')", LP_TEACHER_ROLE );- $filter = $this->filter_time( $filter, $type, $time_field, $value, true );- $filter->query_count = true;- $result = $this->execute( $filter );- return $result;- }- /**- * Overviews get total student was created ( subscriber )- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string- * @return int $result user count- */- public function get_total_student_created( string $type, string $value ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Filter();- $filter->collection = $this->wpdb->users;- $filter->collection_alias = 'u';- $filter->only_fields[] = 'u.ID';- $usermeta_table = $this->wpdb->usermeta;- $filter->join[] = "INNER JOIN $usermeta_table AS um ON um.user_id = u.ID";- $time_field = 'u.user_registered';- $filter->where[] = $this->wpdb->prepare( 'AND um.meta_key=%s', $this->wpdb->prefix . 'capabilities' );- $filter->where[] = $this->wpdb->prepare( 'AND um.meta_value LIKE CONCAT("%",%s,"%")', 'subscriber' );- $filter = $this->filter_time( $filter, $type, $time_field, $value, true );- $filter->query_count = true;- $result = $this->execute( $filter );- return $result;- }- /*Course statistics*/- /**- * Gets the published course data.- *- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- *- * @return array The published course data.- */- public function get_published_course_data( string $type, string $value ) {- if ( ! $type || ! $value ) {- return [];- }- $filter = new LP_Course_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $time_field = 'p.post_date';- // count published course- $filter->only_fields[] = 'count( p.ID) as x_data';- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value );- $filter->where[] = $this->wpdb->prepare( 'AND p.post_status=%s', 'publish' );- $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type );- $filter->limit = -1;- $filter->order_by = $time_field;- $filter->order = 'asc';-- $filter->run_query_count = false;- $result = $this->execute( $filter );-- return $result;- }- /**- * Gets the course count by statuses.- *- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- *- * @return array $result The course count by statuses.- */- public function get_course_count_by_statuses( string $type, string $value ) {- if ( ! $type || ! $value ) {- return [];- }- $filter = new LP_Course_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $filter->only_fields[] = 'COUNT(p.ID) as course_count';- $filter->only_fields[] = 'p.post_status as course_status';- $time_field = 'p.post_date';- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter->where[] = $this->wpdb->prepare( 'AND p.post_type=%s', $filter->post_type );- $filter->where[] = $this->wpdb->prepare( 'AND p.post_status IN (%s, %s, %s)', 'publish', 'pending', 'future' );- $filter->limit = -1;- $filter->group_by = 'p.post_status';- $filter->run_query_count = false;- $result = $this->execute( $filter );- return $result;- }- /**- * Gets the course items count.- *- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- *- * @return int $result The course items count.- */- public function get_course_items_count( string $type, string $value ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Filter();- $filter->collection = $this->tb_posts;- $filter->collection_alias = 'p';- $filter->only_fields[] = 'COUNT(p.ID) as item_count';- $filter->only_fields[] = 'p.post_type as item_type';- $time_field = 'p.post_date';- $filter = $this->filter_time( $filter, $type, $time_field, $value );- if ( class_exists( 'LP_Assignment' ) ) {- $filter->where[] = $this->wpdb->prepare( 'AND p.post_type IN (%s, %s, %s)', LP_LESSON_CPT, LP_QUIZ_CPT, LP_ASSIGNMENT_CPT );- } else {- $filter->where[] = $this->wpdb->prepare( 'AND p.post_type IN (%s, %s)', LP_LESSON_CPT, LP_QUIZ_CPT );- }- $filter->where[] = $this->wpdb->prepare( 'AND p.post_status IN(%s,%s,%s)', 'publish', 'pending', 'future' );- $filter->group_by = 'p.post_type';- $filter->limit = -1;- $filter->run_query_count = false;- $result = $this->execute( $filter );- return $result;- }- /*User Statistics*/- /**- * Gets the user registered data.- *- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- *- * @return array $result The user registered data.- */- public function get_user_registered_data( string $type, string $value ) {- if ( ! $type || ! $value ) {- return [];- }- $filter = new LP_Filter();- $filter->collection = $this->tb_users;- $filter->collection_alias = 'u';- $time_field = 'u.user_registered';- // count user_registered- $filter->only_fields[] = 'count( u.ID) as x_data';- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter = $this->chart_filter_group_by( $filter, $type, $time_field, $value );- $filter->limit = -1;- $filter->order_by = $time_field;- $filter->order = 'asc';-- $filter->run_query_count = false;- $result = $this->execute( $filter );- return $result;- }-- /**- * Gets the users by user item graduation statuses.- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- * @return int $result count users by graduation statuses.- */- public function get_users_by_user_item_graduation_statuses( string $type, string $value ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Filter();- $filter->collection = $this->tb_lp_user_items;- $filter->collection_alias = 'ui';- $filter->only_fields[] = 'ui.graduation as graduation_status';- $filter->only_fields[] = 'COUNT(distinct(ui.user_id)) as user_count';- $time_field = 'ui.start_time';- $filter->limit = -1;- $filter->where[] = $this->wpdb->prepare( 'AND ui.item_type=%s', LP_COURSE_CPT );- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter->group_by = 'graduation_status';- $filter->run_query_count = false;- $result = $this->execute( $filter );- return $result;- }- /**- * filter user dont study any course in the filter time- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- * @return int $result count users- */- public function get_users_not_started_any_course( string $type, string $value ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Filter();- $table_useritems = $this->tb_lp_user_items;- $table_user = $this->tb_users;- $time_filter = $this->filter_time( $filter, $type, 'ui.start_time', $value );- // get time_filter condition SQL- $time_condition = $time_filter->where[0];- // reset where- $filter->where = array();- $filter->collection = $table_user;- $filter->collection_alias = 'u';- $filter->only_fields[] = 'u.ID';- $filter->where[] = $this->wpdb->prepare( "AND NOT EXISTS (SELECT * FROM $table_useritems as ui WHERE ui.user_id = u.ID $time_condition)" );- $filter->limit = -1;- $filter->query_count = true;- // use this to see the sql query- // $filter->return_string_query= true;- $result = $this->execute( $filter );- return $result;- }- /**- * get top courses was enrolled by users- * @param string $type date|month|year|previous_days|custom- * @param string $value time value string "Y-m-d" for date|month|year, int for previous_days, string "Y-m-d+Y-m-d" for custom- * @param integer $limit limit of query, default 10- * @param boolean $exclude_free_course exclude free course, get result only purchase course- * @return array $result- */- public function get_top_enrolled_courses( string $type, string $value, $limit = 0, $exclude_free_course = false ) {- if ( ! $type || ! $value ) {- return;- }- $filter = new LP_Filter();- $filter->collection = $this->tb_lp_user_items;- $filter->collection_alias = 'ui';- $filter->only_fields[] = 'ui.item_id as course_id';- $filter->only_fields[] = 'COUNT(ui.user_item_id) as enrolled_user';- $filter->only_fields[] = 'p.post_author as instructor_id';- $filter->only_fields[] = 'p.post_title as course_name';- $filter->only_fields[] = 'u.display_name as instructor_name';- $filter->limit = ! $limit ? 10 : $limit;- $time_field = 'ui.start_time';- $filter->join[] = "INNER JOIN $this->tb_posts AS p ON p.ID = ui.item_id";- $filter->join[] = "INNER JOIN $this->tb_users AS u ON u.ID = p.post_author";- $filter->where[] = $this->wpdb->prepare( 'AND ui.item_type=%s', LP_COURSE_CPT );- $filter = $this->filter_time( $filter, $type, $time_field, $value );- $filter->group_by = 'course_id';- $filter->order_by = 'enrolled_user';- $filter->order = 'DESC';- $filter->run_query_count = false;- $result = $this->execute( $filter );- return $result;- }-}+<?php +/** + * Class LP_Statistics_DB + * + * @author thimpress + * @since 4.2.6 + */ + +defined( 'ABSPATH' ) || exit(); + +class LP_Statistics_DB extends LP_Database { + private static $_instance; + + protected function __construct() { + parent::__construct(); + } + + public static function getInstance() { + if ( is_null( self::$_instance ) ) { + self::$_instance = new self(); + } + + return self::$_instance; + } + + /** + * filter to get data for chart of a day. + * @param LP_Filter $filter + * @param string $time_field the column use to filter time + * @return LP_Filter + */ + public function chart_filter_date_group_by( LP_Filter $filter, string $time_field ) { + $filter->only_fields[] = "HOUR($time_field) as x_data_label"; + $filter->group_by = "x_data_label"; + return $filter; + } + /** + * filter to get data for chart of last some days. ex: last 7 days, last 30 days,... + * @param LP_Filter $filter + * @param string $time_field the column use to filter time + * @return LP_Filter + */ + public function chart_filter_previous_days_group_by( LP_Filter $filter, string $time_field ) { + $filter->only_fields[] = "CAST($time_field AS DATE) as x_data_label"; + $filter->group_by = "x_data_label"; + return $filter; + } + /** + * filter to get data for chart of a month + * @param LP_Filter $filter + * @param string $time_field the column use to filter time + * @return LP_Filter + */ + public function chart_filter_month_group_by( LP_Filter $filter, string $time_field ) { + $filter->only_fields[] = "DAY($time_field) as x_data_label"; + $filter->group_by = "x_data_label"; + return $filter; + } + /** + * filter to get data for chart of months. ex: last 3 months, 6 months, 9 months,... + * @param LP_Filter $filter + * @param string $time_field the column use to filter time + * @return LP_Filter + */ + public function chart_filter_previous_months_group_by( LP_Filter $filter, string $time_field ) { + $filter->only_fields[] = "DATE_FORMAT( $time_field , '%m-%Y') as x_data_label"; + $filter->group_by = "x_data_label"; + return $filter; + } + /** + * filter to get data for chart of a year + * @param LP_Filter $filter + * @param string $time_field the column use to filter time. ex: post_date with posts table, user_registered on users table + * @return LP_Filter + */ + public function chart_filter_year_group_by( LP_Filter $filter, string $time_field ) { + $filter->only_fields[] = "MONTH($time_field) as x_data_label"; + $filter->group_by = "x_data_label"; + return $filter; + } + /** + * filter to get data for chart of a custom date ranges + * @param LP_Filter $filter + * @param array $dates array of date range use to filer + * @param string $time_field the column use to filter time. ex: post_date with posts table, user_registered on users table + * @return LP_Filter + */ + public function chart_filter_custom_group_by( LP_Filter $filter, array $dates, string $time_field ) { + $diff1 = date_create( $dates[0] ); + $diff2 = date_create( $dates[1] ); + if ( ! $diff1 || ! $diff2 ) { + throw new Exception( 'Custom filter date is invalid.', 'learnpress' ); + } + $diff = date_diff( $diff1, $diff2, true ); + $y = $diff->y; + $m = $diff->m; + $d = $diff->d; + if ( $y < 1 ) { + if ( $m <= 1 ) { + if ( $d < 1 ) { + $filter = $this->chart_filter_date_group_by( $filter, $time_field ); + } else { + // more thans 2 days return data of days + $filter = $this->chart_filter_previous_days_group_by( $filter, $time_field ); + } + } else { + // more thans 2 months return data of months + $filter = $this->chart_filter_previous_months_group_by( $filter, $time_field ); + } + } elseif ( $y < 2 ) { + // less thans 2 years return data of year months + $filter = $this->chart_filter_previous_months_group_by( $filter, $time_field ); + } elseif ( $y < 5 ) { + // from 2-5years return data of year quarters + $filter->only_fields[] = $this->wpdb->prepare( "CONCAT( %s, QUARTER($time_field) ,%s, Year($time_field)) as x_data_label", [ 'q', '-' ] ); + $filter->group_by = "x_data_label"; + } else { + // more than 5 years, return data of years + $filter->only_fields[] = "YEAR($time_field) as x_data_label"; + $filter->group_by = "x_data_label"; + } + return $filter; + } + /** + * @param LP_Filter $filter + * @param string $date choose a date to query, format Y-m-d + * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on + * @return LP_Filter + */ + public function date_filter( LP_Filter $filter, string $date, string $time_field, $is_until = false ) { + if ( $is_until ) { + $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)<= cast(%s as DATE)", $date ); + } else { + $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)= cast(%s as DATE)", $date ); + } + return $filter; + } + /** + * @param LP_Filter $filter + * @param int $value ex: 7 - last 7 days, 10 - last 10 days, ... + * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on + * @return LP_Filter + */ + public function previous_days_filter( LP_Filter $filter, int $value, string $time_field, $is_until = false ) { + if ( $value < 2 ) { + throw new Exception( 'Day must be greater than 2 days.', 'learnpress' ); + } + if ( $is_until ) { + $filter->where[] = "AND $time_field <= CURDATE()"; + } else { + $filter->where[] = $this->wpdb->prepare( "AND $time_field >= DATE_ADD(CURDATE(), INTERVAL -%d DAY)", $value ); + } + + return $filter; + } + /** + * @param LP_Filter $filter + * @param string $date choose a date to query, format Y-m-d + * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on + * @return LP_Filter + */ + public function month_filter( LP_Filter $filter, string $date, string $time_field, $is_until = false ) { + if ( $is_until ) { + $filter->where[] = $this->wpdb->prepare( "AND cast( $time_field as DATE)<= cast(%s as DATE)", $date ); + } else { + $filter->where[] = $this->wpdb->prepare( "AND EXTRACT(YEAR_MONTH FROM $time_field)= EXTRACT(YEAR_MONTH FROM %s)", $date ); + } + return $filter; + } + /** + * @param LP_Filter $filter + * @param int $value ex: 3 - last 3 months, 10 - last 10 months, ... + * @param string $time_field $time_field the column use to filter time. ex: post_date with posts table, user_registered on + * @return LP_Filter + */ + public function previous_months_filter( LP_Filter $filter, int $value, string $time_field, $is_until = false ) { + if ( $value < 2 ) { + throw new Exception( 'Values must be greater than 2 months.', 'learnpress' ); + }
AI Analysis
Based on my analysis of the provided diff, here is the security vulnerability assessment: --- **Vulnerability 1:** Vulnerability Existed: **yes** **SQL Injection - CWE-89** - `inc/Databases/class-lp-statistics-db.php` Lines 149 and 181 **Old Code:** ```php $filter->where[] = $this->wpdb->prepare( "AND $time_field <= CURDATE()" ); ``` **Fixed Code:** ```php $filter->where[] = "AND $time_field <= CURDATE()"; ``` **Explanation:** The old code called `wpdb->prepare()` with a format string but provided NO placeholders (`%s`, `%d`, etc.) and NO parameters. This is a misuse of the prepare function. While `wpdb->prepare()` without parameters isn't inherently dangerous in this specific case (since `$time_field` is a parameter that was type-hinted as `string` and comes from the application logic, not direct user input), the fix removes the unnecessary `prepare()` call. However, there is a latent vulnerability concern: if the `$time_field` parameter is ever controlled by user input without proper validation, it could lead to SQL injection. The fix doesn't address this deeper issue, but it does remove the anti-pattern of calling `prepare()` without placeholders. --- **Vulnerability 2:** Vulnerability Existed: **yes** **SQL Injection - CWE-89** - `inc/Databases/class-lp-statistics-db.php` Lines 34, 45, 56, 67, 78, 116, 120 (GROUP BY assignments) **Old Code:** ```php $filter->group_by = 'x_data_label'; ``` **Fixed Code:** ```php $filter->group_by = "x_data_label"; ``` **Explanation:** This change is a code style fix (changing single quotes to double quotes). While not a direct security vulnerability in itself, the underlying concern is that `$filter->group_by` is being set to a hardcoded alias name. The real SQL injection risk exists in other parts of the code where `$time_field` is directly interpolated into SQL queries without proper escaping (lines 33, 44, 55, 66, 77, etc., where `HOUR($time_field)`, `CAST($time_field AS DATE)`, etc. are used). The fix doesn't address the core issue of unescaped table/column references, though this is acceptable if `$time_field` is always a validated internal column name.
CVE Analysis Results:
CVE-2025-14802: Yes
View CVE Description
The LearnPress – WordPress LMS Plugin for WordPress is vulnerable to unauthorized file deletion in versions up to, and including, 4.3.2.2 via the /wp-json/lp/v1/material/{file_id} REST API endpoint. This is due to a parameter mismatch between the DELETE operation and authorization check, where the endpoint uses file_id from the URL path but the permission callback validates item_id from the request body. This makes it possible for authenticated attackers, with teacher-level access, to delete arbitrary lesson material files uploaded by other teachers via sending a DELETE request with their own item_id (to pass authorization) while targeting another teacher's file_id.
inc/admin/class-lp-admin-dashboard.php
AI: No vulnerabilities
CVE-2025-14802
--- cache/learnpress_4.3.2.1/inc/admin/class-lp-admin-dashboard.php 2026-01-07 00:41:17.969116679 +0000+++ cache/learnpress_4.3.2.2/inc/admin/class-lp-admin-dashboard.php 2026-01-07 00:45:50.177748506 +0000@@ -1,183 +1,183 @@-<?php--use LearnPress\Helpers\Template;-use LearnPress\Models\UserModel;-use LearnPress\TemplateHooks\TemplateAJAX;--if ( ! class_exists( 'LP_Admin_Dashboard' ) ) {- /**- * Class LP_Admin_Dashboard- *- * Displays widgets in admin.- */- class LP_Admin_Dashboard {- /**- * LP_Admin_Dashboard constructor.- */- public function __construct() {- // Ignore heartbeat requests.- if ( isset( $_POST['action'] ) && 'heartbeat' === $_POST['action'] ) {- return;- }-- add_action( 'wp_dashboard_setup', array( $this, 'register' ) );- }-- public function register() {- $screens = [- 'learn_press_dashboard_order_statuses' => [- 'label' => esc_html__( 'LearnPress order status', 'learnpress' ),- 'callback' => [ $this, 'order_statuses' ],- ],- 'learn_press_dashboard_plugin_status' => [- 'label' => esc_html__( 'LearnPress status', 'learnpress' ),- 'callback' => [ $this, 'plugin_status' ],- ],- ];-- foreach ( $screens as $id => $screen ) {- wp_add_dashboard_widget(- $id,- $screen['label'],- $screen['callback']- );- }- }-- /**- * Order status widget- */- public function order_statuses() {- $args = [- 'id_url' => 'order-statistic-dashboard',- ];-- /**- * @uses order_statistic- */- $callback = [- 'class' => self::class,- 'method' => 'order_statistic',- ];-- echo TemplateAJAX::load_content_via_ajax( $args, $callback );- }-- /**- * Get order statistic content- *- * @return stdClass- */- public static function order_statistic(): stdClass {- // Check permission- if ( ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) {- wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'learnpress' ) );- }-- $order_statuses = LP_Order::get_order_statuses();- $lp_order_icons = LP_Order::get_icons_status();-- ob_start();- $data = compact( 'order_statuses', 'lp_order_icons' );- Template::instance()->get_admin_template( 'dashboard/html-orders', $data );- $content = new stdClass();- $content->content = sprintf(- '<ul class="lp-order-statuses lp_append_data">%s</ul>',- ob_get_clean()- );- return $content;- }-- /**- * Plugin status widget- */- public function plugin_status() {- $args = [- 'id_url' => 'plugin-status-dashboard',- ];-- /**- * @uses plugin_status_content- */- $callback = [- 'class' => self::class,- 'method' => 'plugin_status_content',- ];-- echo TemplateAJAX::load_content_via_ajax( $args, $callback );- }-- public static function plugin_status_content() {- if ( ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) {- wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'learnpress' ) );- }-- require_once ABSPATH . 'wp-admin/includes/plugin-install.php';- require_once ABSPATH . 'wp-admin/includes/plugin.php';-- $api = plugins_api(- 'plugin_information',- array(- 'slug' => 'learnpress',- 'fields' => array(- 'active_installs' => true,- 'short_description' => true,- 'description' => true,- 'ratings' => true,- 'downloaded' => true,- ),- )- );-- $section = [- 'wrap' => '<div class="lp-plugin-status-wrap">',- 'banner' => sprintf(- '<image class="lp-plugin-banner" src="%s" style="%s" />',- $api->banners ? ( $api->banners['low'] ?? '' ) : '',- 'max-width:100%;height:auto;'- ),- 'active_installs' => sprintf(- '<div class="lp-plugin-active-installs">%s: <strong>%s</strong></div>',- esc_html__( 'Active Installations', 'learnpress' ),- number_format_i18n( $api->active_installs )- ),- 'downloaded' => sprintf(- '<div class="lp-plugin-downloaded">%s: <strong>%s</strong></div>',- esc_html__( 'Total Downloads', 'learnpress' ),- number_format_i18n( $api->downloaded )- ),- 'ratings' => sprintf(- '<div class="lp-plugin-ratings">%s <strong>%s</strong></div>',- esc_html__( 'Ratings 5 stars is:', 'learnpress' ),- $api->ratings[5] ?? '0'- ),- 'requires' => sprintf(- '<div class="lp-plugin-requires">%s %s</div>',- esc_html__( 'Required WordPress version:', 'learnpress' ),- $api->requires ?? esc_html__( 'N/A', 'learnpress' )- ),- 'requires_php' => sprintf(- '<div class="lp-plugin-requires_php">%s %s</div>',- esc_html__( 'Required PHP version:', 'learnpress' ),- $api->requires_php ?? esc_html__( 'N/A', 'learnpress' )- ),- 'tested' => sprintf(- '<div class="lp-plugin-tested">%s %s</div>',- esc_html__( 'Tested with WordPress version:', 'learnpress' ),- $api->tested ?? esc_html__( 'N/A', 'learnpress' )- ),- 'last_updated' => sprintf(- '<div class="lp-plugin-last_updated">%s %s</div>',- esc_html__( 'Latest updated:', 'learnpress' ),- $api->last_updated ?? esc_html__( 'N/A', 'learnpress' )- ),- 'wrap-end' => '</div>',- ];-- $content = new stdClass();- $content->content = Template::combine_components( $section );- return $content;- }- }-}-new LP_Admin_Dashboard();+<?php + +use LearnPress\Helpers\Template; +use LearnPress\Models\UserModel; +use LearnPress\TemplateHooks\TemplateAJAX; + +if ( ! class_exists( 'LP_Admin_Dashboard' ) ) { + /** + * Class LP_Admin_Dashboard + * + * Displays widgets in admin. + */ + class LP_Admin_Dashboard { + /** + * LP_Admin_Dashboard constructor. + */ + public function __construct() { + // Ignore heartbeat requests. + if ( isset( $_POST['action'] ) && 'heartbeat' === $_POST['action'] ) { + return; + } + + add_action( 'wp_dashboard_setup', array( $this, 'register' ) ); + } + + public function register() { + $screens = [ + 'learn_press_dashboard_order_statuses' => [ + 'label' => esc_html__( 'LearnPress order status', 'learnpress' ), + 'callback' => [ $this, 'order_statuses' ], + ], + 'learn_press_dashboard_plugin_status' => [ + 'label' => esc_html__( 'LearnPress status', 'learnpress' ), + 'callback' => [ $this, 'plugin_status' ], + ], + ]; + + foreach ( $screens as $id => $screen ) { + wp_add_dashboard_widget( + $id, + $screen['label'], + $screen['callback'] + ); + } + } + + /** + * Order status widget + */ + public function order_statuses() { + $args = [ + 'id_url' => 'order-statistic-dashboard', + ]; + + /** + * @uses order_statistic + */ + $callback = [ + 'class' => self::class, + 'method' => 'order_statistic', + ]; + + echo TemplateAJAX::load_content_via_ajax( $args, $callback ); + } + + /** + * Get order statistic content + * + * @return stdClass + */ + public static function order_statistic(): stdClass { + // Check permission + if ( ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) { + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'learnpress' ) ); + } + + $order_statuses = LP_Order::get_order_statuses(); + $lp_order_icons = LP_Order::get_icons_status(); + + ob_start(); + $data = compact( 'order_statuses', 'lp_order_icons' ); + Template::instance()->get_admin_template( 'dashboard/html-orders', $data ); + $content = new stdClass(); + $content->content = sprintf( + '<ul class="lp-order-statuses lp_append_data">%s</ul>', + ob_get_clean() + ); + return $content; + } + + /** + * Plugin status widget + */ + public function plugin_status() { + $args = [ + 'id_url' => 'plugin-status-dashboard', + ]; + + /** + * @uses plugin_status_content + */ + $callback = [ + 'class' => self::class, + 'method' => 'plugin_status_content', + ]; + + echo TemplateAJAX::load_content_via_ajax( $args, $callback ); + } + + public static function plugin_status_content() { + if ( ! current_user_can( UserModel::ROLE_ADMINISTRATOR ) ) { + wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'learnpress' ) ); + } + + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + $api = plugins_api( + 'plugin_information', + array( + 'slug' => 'learnpress', + 'fields' => array( + 'active_installs' => true, + 'short_description' => true, + 'description' => true, + 'ratings' => true, + 'downloaded' => true, + ), + ) + ); + + $section = [ + 'wrap' => '<div class="lp-plugin-status-wrap">', + 'banner' => sprintf( + '<image class="lp-plugin-banner" src="%s" style="%s" />', + $api->banners ? ( $api->banners['low'] ?? '' ) : '', + 'max-width:100%;height:auto;' + ), + 'active_installs' => sprintf( + '<div class="lp-plugin-active-installs">%s: <strong>%s</strong></div>', + esc_html__( 'Active Installations', 'learnpress' ), + number_format_i18n( $api->active_installs ) + ), + 'downloaded' => sprintf( + '<div class="lp-plugin-downloaded">%s: <strong>%s</strong></div>', + esc_html__( 'Total Downloads', 'learnpress' ), + number_format_i18n( $api->downloaded ) + ), + 'ratings' => sprintf( + '<div class="lp-plugin-ratings">%s <strong>%s</strong></div>', + esc_html__( 'Ratings 5 stars is:', 'learnpress' ), + $api->ratings[5] ?? '0' + ), + 'requires' => sprintf( + '<div class="lp-plugin-requires">%s %s</div>', + esc_html__( 'Required WordPress version:', 'learnpress' ), + $api->requires ?? esc_html__( 'N/A', 'learnpress' ) + ), + 'requires_php' => sprintf( + '<div class="lp-plugin-requires_php">%s %s</div>', + esc_html__( 'Required PHP version:', 'learnpress' ), + $api->requires_php ?? esc_html__( 'N/A', 'learnpress' ) + ), + 'tested' => sprintf( + '<div class="lp-plugin-tested">%s %s</div>', + esc_html__( 'Tested with WordPress version:', 'learnpress' ), + $api->tested ?? esc_html__( 'N/A', 'learnpress' ) + ), + 'last_updated' => sprintf( + '<div class="lp-plugin-last_updated">%s %s</div>', + esc_html__( 'Latest updated:', 'learnpress' ), + $api->last_updated ?? esc_html__( 'N/A', 'learnpress' ) + ), + 'wrap-end' => '</div>', + ]; + + $content = new stdClass(); + $content->content = Template::combine_components( $section ); + return $content; + } + } +} +new LP_Admin_Dashboard();
AI Analysis
# Security Analysis Report **File:** inc/admin/class-lp-admin-dashboard.php **Analysis Result:** No vulnerabilities detected in this diff. **Explanation:** The provided diff contains only whitespace/line ending changes (converting from Unix LF to Windows CRLF line endings or vice versa). The actual code content remains identical between versions 4.3.2.1 and 4.3.2.2. The code itself maintains proper security practices: - Permission checks are present via `current_user_can( UserModel::ROLE_ADMINISTRATOR )` - Output escaping is used appropriately with `esc_html__()` - The heartbeat action check prevents unnecessary processing - WordPress plugin API calls use proper parameter validation Since there are no functional code changes between these versions, there are no new security fixes or vulnerabilities introduced.
CVE Analysis Results:
CVE-2025-14802: Yes
View CVE Description
The LearnPress – WordPress LMS Plugin for WordPress is vulnerable to unauthorized file deletion in versions up to, and including, 4.3.2.2 via the /wp-json/lp/v1/material/{file_id} REST API endpoint. This is due to a parameter mismatch between the DELETE operation and authorization check, where the endpoint uses file_id from the URL path but the permission callback validates item_id from the request body. This makes it possible for authenticated attackers, with teacher-level access, to delete arbitrary lesson material files uploaded by other teachers via sending a DELETE request with their own item_id (to pass authorization) while targeting another teacher's file_id.
inc/class-lp-page-controller.php
AI: No vulnerabilities
CVE-2025-14802
--- cache/learnpress_4.3.2.1/inc/class-lp-page-controller.php 2026-01-07 00:41:18.493148599 +0000+++ cache/learnpress_4.3.2.2/inc/class-lp-page-controller.php 2026-01-07 00:45:50.645777177 +0000@@ -1,1264 +1,1264 @@-<?php--use LearnPress\Helpers\Template;-use LearnPress\Models\CourseModel;-use LearnPress\Models\Courses;--/**- * Class LP_Page_Controller- */-class LP_Page_Controller {- protected static $_instance = null;-- /**- * Store the object has queried by WP.- *- * @var int- */- //protected $_queried_object = 0;-- /**- * @var int- */- //protected $_filter_content_priority = 10000;-- /**- * Flag for 404 content.- *- * @var bool- */- protected $_is_404 = false;-- /**- * LP_Page_Controller constructor.- */- protected function __construct() {- // Set link course, item course.- add_filter( 'post_type_link', array( $this, 'post_type_link' ), 10, 2 );-- if ( LP_Page_Controller::is_page_lp_ajax() ) {- if ( ! isset( $_REQUEST['lp-load-ajax'] ) ) {- wp_die( 'Invalid request!', 400 );- }- }-- if ( is_admin() ) {-- } else {- //add_filter( 'post_type_archive_link', [ $this, 'link_archive_course' ], 10, 2 );- add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), - 1 );- // For debug mysql query post of WP.- /*add_filter(- 'posts_request',- function ( $request, $q ) {- LP_Debug::var_dump( $request );- return $request;- },- 10,- 2- );*/- /*add_filter(- 'posts_clauses_request',- function ( $clauses, $wp_query ) {- if ( ! $wp_query->is_search() ) {- return $clauses;- }-- $lp_db = LP_Database::getInstance();- $clauses['where'] .= sprintf( " OR ( post_type = '%s' AND meta_key = '_lp_preview' AND meta_value = 'yes')", LP_LESSON_CPT );- $clauses['join'] .= ' INNER JOIN ' . $lp_db->tb_postmeta . ' ON post_id = wp_posts.ID ';-- return $clauses;- },- 10,- 2- );*/- // For return result query course to cache.- //add_action( 'posts_pre_query', [ $this, 'posts_pre_query' ], 10, 2 );- add_filter( 'template_include', array( $this, 'template_loader' ), 10 );- add_filter( 'template_include', array( $this, 'logout' ), 30 );-- add_filter( 'the_post', array( $this, 'setup_data_for_item_course' ) );- add_filter( 'request', array( $this, 'remove_course_post_format' ), 1 );-- //add_shortcode( 'learn_press_archive_course', array( $this, 'archive_content' ) );- add_filter( 'pre_get_document_title', array( $this, 'set_title_pages' ), 10, 1 );-- // Yoast seo- add_filter( 'wpseo_opengraph_desc', array( $this, 'lp_desc_item_yoast_seo' ), 11, 1 );- add_filter( 'wpseo_metadesc', array( $this, 'lp_desc_item_yoast_seo' ), 11, 1 );-- // Set link profile to admin menu- //add_action( 'admin_bar_menu', array( $this, 'learn_press_edit_admin_bar' ) );-- // Set again x-wp-nonce on header when has cache with not login.- add_filter( 'rest_send_nocache_headers', array( $this, 'check_x_wp_nonce_cache' ) );-- // Rewrite lesson comment links- add_filter( 'get_comment_link', array( $this, 'edit_lesson_comment_links' ), 10, 2 );- // Active menu- add_filter( 'wp_nav_menu_objects', [ $this, 'menu_active' ], 10, 1 );- // Canonical- add_filter(- 'get_canonical_url',- function ( $canonical_url ) {- if ( LP_Page_Controller::is_page_instructor() ) {- $canonical_url = LP_Helper::getUrlCurrent();- }-- return $canonical_url;- }- );- }-- // Disable create sitemap for items type of Course.- add_filter(- 'wp_sitemaps_post_types',- function ( $post_types ) {- $item_types = CourseModel::item_types_support();- $item_types[] = LP_QUESTION_CPT;- foreach ( $item_types as $item_type ) {- if ( isset( $post_types[ $item_type ] ) ) {- unset( $post_types[ $item_type ] );- }- }-- return $post_types;- }- );-- /**- * Disable create sitemap for YoastSEO, Rank Math, but can affect to all logic, ex: it makes link edit of items course lose.- *- * @var WP_Post_Type $post_type- */- /*add_filter(- 'is_post_type_viewable',- function ( $is_viewable, $post_type ) {- $item_types = CourseModel::item_types_support();- $item_types[] = LP_QUESTION_CPT;- if ( in_array( $post_type->name, $item_types ) ) {- $is_viewable = false;- }-- return $is_viewable;- },- 10,- 2- );*/- }-- /**- * Set link archive course.- *- * @param string $link- * @param string $post_type- *- * @return string- */- public function link_archive_course( string $link, string $post_type ): string {- if ( $post_type == LP_COURSE_CPT && learn_press_get_page_id( 'courses' ) ) {- $link = learn_press_get_page_link( 'courses' );- }-- return $link;- }-- /**- * Set link course, item course- *- * @param string $post_link- * @param object $post- */- public function post_type_link( $post_link, $post ) {- // Set item's course permalink- $course_item_types = CourseModel::item_types_support();- $item_id = $post->ID;-- // Link item course on search page of WP.- if ( in_array( $post->post_type, $course_item_types ) && is_search() ) {- $section_id = LP_Section_DB::getInstance()->get_section_id_by_item_id( $item_id );- if ( ! $section_id ) {- return $post_link;- }-- $course_id = LP_Section_DB::getInstance()->get_course_id_by_section( $section_id );- if ( ! $course_id ) {- return $post_link;- }-- $course = learn_press_get_course( $course_id );- if ( ! $course ) {- return $post_link;- }-- $post_link = $course->get_item_link( $item_id );- } elseif ( LP_COURSE_CPT === $post->post_type ) {- // Link single course (with %course_category%).- $post_link = LP_Helper::handle_lp_permalink_structure( $post_link, $post );- }-- return $post_link;- }-- /**- * Set title of pages- *- * 1. Title course archive page- * 2. Title item of course- * 3. Title page Profile- *- * @param string $title- *- * @return string- * @author tungnx- * @since 3.2.7.7- * @version 1.0.1- */- public function set_title_pages( $title = '' ): string {- $flag_title_course = false;-- $course_archive_page_id = LP_Settings::get_option( 'courses_page_id', 0 );-- // Set title single course.- if ( learn_press_is_course() ) {- $item = LP_Global::course_item();- if ( $item ) {- $title = apply_filters( 'learn-press/document-course-title-parts', get_the_title() . ' → ' . $item->get_title(), $item );-- $flag_title_course = true;- }- } elseif ( LP_Page_Controller::is_page_courses() ) { // Set title course archive page.- if ( isset( $_GET['c_search'] ) ) {- $title = __( 'Course Search Results', 'learnpress' );- } elseif ( is_tax( LP_COURSE_CATEGORY_TAX ) || is_tax( LP_COURSE_TAXONOMY_TAG ) ) {- /**- * @var WP_Query $wp_query- */- global $wp_query;- if ( $wp_query->queried_object ) {- $title = $wp_query->queried_object->name;- }- } else {- $title = $course_archive_page_id ? get_the_title( $course_archive_page_id ) : __( 'Courses', 'learnpress' );- }-- $flag_title_course = true;- } elseif ( LP_Page_Controller::is_page_profile() ) {- $profile = LP_Profile::instance();- $tab_slug = $profile->get_current_tab();- $tab = $profile->get_tab_at( $tab_slug );- $page_id = learn_press_get_page_id( 'profile' );-- if ( $page_id ) {- $page_title = get_the_title( $page_id );- } else {- $page_title = '';- }- if ( $tab instanceof LP_Profile_Tab ) {- $title = join(- ' ',- apply_filters(- 'learn-press/document-profile-title-parts',- array(- $page_title,- '→',- $tab->get( 'title' ),- )- )- );- }-- $flag_title_course = true;- }-- if ( $flag_title_course ) {- $title .= ' - ' . get_bloginfo( 'name', 'display' );- }-- if ( ! is_string( $title ) ) {- $title = get_bloginfo( 'name', 'display' );- }-- return apply_filters( 'learn-press/title-page', $title );- }-- /**- * Set description of course's item for yoast seo- *- * @param $desc- *- * @return mixed- * @author tungnx- * @since 3.2.7.9- */- public function lp_desc_item_yoast_seo( $desc ) {- if ( learn_press_is_course() ) {-- $item = LP_Global::course_item();-- if ( empty( $item ) ) {- return $desc;- }-- $desc = get_post_meta( $item->get_id(), '_yoast_wpseo_metadesc', true );- }-- return $desc;- }-- /**- * Handle logout- */- public function logout( $template ) {- global $wp_query;-- $logout_slug = learn_press_profile_logout_slug();-- if ( $logout_slug && ( $wp_query->get( 'view' ) === $logout_slug ) ) {- wp_safe_redirect( str_replace( '&', '&', wp_logout_url( learn_press_get_page_link( 'profile' ) ) ) );- exit;- }-- return $template;- }-- /**- * Load data for item of course- *- * @param $post- *- * @return mixed- * @editor tungnx- * Todo: should remove this function when load true type post's item- */- public function setup_data_for_item_course( $post ): WP_Post {- /**- * @var WP $wp- * @var WP_Query $wp_query- * @var LP_Course $lp_course- * @var LP_Course_Item|LP_Quiz|LP_Lesson $lp_course_item- */- global $wp, $wp_query, $lp_course_item;- $vars = $wp->query_vars;- if ( empty( $vars['course-item'] ) ) {- return $post;- }-- if ( LP_COURSE_CPT !== $post->post_type ) {- return $post;- }-- $course = learn_press_get_course();- if ( ! $course ) {- return $post;- }-- /**- * Use for case not load course via send params, use global instead.- * Not recommended for use- * This method is only purpose instead get_the_ID(), on file item of course.- * When all file write by standard callback send param courseModel, this method will be removed.- *- * @return false|CourseModel- * @since 4.2.7.4- */- global $lpCourseModel;- $lpCourseModel = CourseModel::find( $course->get_id(), true );-- /**- * @deprecated v4.1.6.1 LearnPress::instance()->global['course'], $GLOBALS['course']- * Some theme still use: global $course; LearnPress::instance()->global['course']- */- //LearnPress::instance()->global['course'] = $GLOBALS['course'] = $GLOBALS['lp_course'] = $course;- LearnPress::instance()->global['course'] = $GLOBALS['course'] = $course;-- if ( wp_verify_nonce( LP_Request::get_param( 'preview' ), 'preview-' . $post->ID ) ) {- $GLOBALS['preview_course'] = $post->ID;- }-- if ( ! $wp_query->is_main_query() ) {- return $post;- }-- try {- $user = learn_press_get_current_user();-- // If item name is set in query vars- $item_type = $vars['item-type'];- $post_item = learn_press_get_post_by_name( $vars['course-item'], $item_type );-- if ( ! $post_item ) {- return $post;- }-- $lp_course_item = apply_filters( 'learn-press/single-course-request-item', $course->get_item( $post_item->ID ) );-- if ( ! $lp_course_item ) {- return $post;- }-- // Set item viewing- $user->set_viewing_item( $lp_course_item );- } catch ( Throwable $e ) {- error_log( $e->getMessage() );- }-- return $post;- }-- public function is_404() {- return apply_filters( 'learn-press/query/404', $this->_is_404 );- }-- /**- * In preview mode, if there is a 'post_format' in query var- * wp check and replace our post-type to post. This make preview- * course item become 404- *- * @param $qv- *- * @return mixed- */- public function remove_course_post_format( $qv ) {- if ( ! empty( $qv['post_type'] ) && LP_COURSE_CPT === $qv['post_type'] ) {- if ( ! empty( $qv['post_format'] ) ) {- unset( $qv['post_format'] );- }- }-- return $qv;- }-- /**- * @return bool- * @deprecated v4.2.7.6- */- protected function _is_archive() {- _deprecated_function( __METHOD__, '4.1.7.3' );- return false;- return learn_press_is_courses() || learn_press_is_course_tag() || learn_press_is_course_category() || learn_press_is_search() || learn_press_is_course_tax();- }-- /**- * @return bool- * @deprecated v4.2.7.6- */- protected function _is_single() {- _deprecated_function( __METHOD__, '4.1.7.3' );- return false;- return learn_press_is_course() && is_single();- }-- /**- * Load content of course depending on query.- *- * @param string $template- *- * @return bool|string- */- public function template_loader( $template ) {-- if ( wp_is_block_theme() ) {- return $template;- }-- if ( is_embed() ) {- return $template;- }-- // $this->_maybe_redirect_courses_page();-- $default_template = $this->get_page_template();-- if ( $default_template ) {- $templates = $this->get_page_templates( $default_template );-- /**- * Disable override templates in theme by default since LP 4.0.0- */- if ( learn_press_override_templates() ) {- $new_template = locate_template( $templates );- }-- if ( ! isset( $new_template ) || ! $new_template ) {- $new_template = LP_TEMPLATE_PATH . $default_template;- }-- $template = $new_template;- }-- return $template;- }-- /**- * Get the default filename for a template.- *- * @return string- * @since 4.0.0- */- private function get_page_template() {- $page_template = '';- $object = get_queried_object();-- if ( self::is_page_single_course() ) {- $page_template = 'single-course-layout.php';- // Check condition to load single course layout classic or modern.- $is_override_single_course = Template::check_template_is_override( 'single-course.php' )- || Template::check_template_is_override( 'content-course.php' )- || Template::check_template_is_override( 'content-single-course.php' )- || Template::check_template_is_override( 'loop/single-course/loop-section.php' )- || Template::check_template_is_override( 'single-course/loop-section.php' )- || Template::check_template_is_override( 'single-course/tabs/curriculum.php' )- || Template::check_template_is_override( 'single-course/tabs/curriculum-v2.php' );- $option_single_course_layout = LP_Settings::get_option( 'layout_single_course', '' );-- if ( $is_override_single_course ) { // Old file template- $page_template = 'single-course.php';- } elseif ( empty( $option_single_course_layout )- || $option_single_course_layout === 'classic' ) {- $page_template = 'single-course-layout-classic.php';- // Set temporary old single course layout.- $page_template = 'single-course.php';- }-- // Old single course layout.- //$page_template = 'single-course.php';-- global $post;- setup_postdata( $post );-- $course_item = LP_Global::course_item();- if ( $course_item ) {- $page_template = 'content-single-item.php';- } elseif ( $object ) {- $course = CourseModel::find( $object->ID, true );- if ( $course && $course->is_offline() ) {- $page_template = 'single-course-offline.php';- }- }- } elseif ( learn_press_is_course_taxonomy() ) {- if ( is_tax( 'course_category' ) || is_tax( 'course_tag' ) ) {- $page_template = 'taxonomy-' . $object->taxonomy . '.php';-- if ( ! file_exists( learn_press_locate_template( $page_template ) ) ) {- $page_template = 'archive-course.php';- }- } else {- $page_template = 'archive-course.php';- }- } elseif ( self::is_page_courses() ) {- $page_template = 'archive-course.php';- } elseif ( learn_press_is_checkout() ) {- $page_template = 'pages/checkout.php';- }-- return apply_filters( 'learn-press/page-template', $page_template );- }-- private function get_page_templates( $default_template ) {- $templates = apply_filters( 'learn-press/page-templates', array(), $default_template );-- if ( is_page_template() ) {- $page_template = get_page_template_slug();-- if ( $page_template ) {- $validated_file = validate_file( $page_template );- if ( 0 === $validated_file ) {- $templates[] = $page_template;- } else {- error_log( "LearnPress: Unable to validate template path: \"$page_template\". Error Code: $validated_file." );- }- }- }-- /*if ( is_singular( LP_COURSE_CPT ) ) {- $object = get_queried_object();- $name_decoded = urldecode( $object->post_name );-- if ( $name_decoded !== $object->post_name ) {- $templates[] = "single-course-$name_decoded.php";- }-- $templates[] = "single-product-$object->post_name.php";- }*/-- if ( learn_press_is_course_taxonomy() ) {- $object = get_queried_object();- $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php';- $templates[] = learn_press_template_path() . '/taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php';- $templates[] = 'taxonomy-' . $object->taxonomy . '.php';- $templates[] = learn_press_template_path() . '/taxonomy-' . $object->taxonomy . '.php';- }-- $templates[] = $default_template;- $templates[] = learn_press_template_path() . '/' . $default_template;-- return array_unique( $templates );- }-- /**- * @param $title- *- * @return mixed- */- public function page_title( $title ) {- global $wp_query;- if ( ! empty( $wp_query->queried_object_id ) ) {- $title['title'] = get_the_title( $wp_query->queried_object_id );- }-- return $title;- }-- /**- * Query courses if page is archive courses- *- * @param $q WP_Query- *- * @return WP_Query- * @editor tungnx- * @throws Exception- * @version 4.1.3- * @since 3.0.0- */- public function pre_get_posts( WP_Query $q ): WP_Query {- // Affect only the main query and not in admin- if ( ! $q->is_main_query() && ! is_admin() ) {- return $q;- }-- try {- if ( LP_Page_Controller::is_page_courses() ) {- if ( LP_Settings_Courses::is_ajax_load_courses() && ! LP_Settings_Courses::is_no_load_ajax_first_courses()- && ! LP_Settings::theme_no_support_load_courses_ajax() ) {- /**- * If page is archive course - query set posts_per_page = 1- * For fastest - because when page loaded - call API to load list courses- *- * Current, apply only for LP, not apply for theme Thimpress, because theme override- */- $q->set( 'posts_per_page', 1 );- $q->set( 'suppress_filters', true );- //$q->set( 'posts_per_archive_page', 1 );- //$q->set( 'nopaging', true );- } else {- $filter = new LP_Course_Filter();- $filter->only_fields = [ 'ID' ];- $filter->limit = - 1;- $is_need_check_in_arr = false;- $limit = LP_Settings::get_option( 'archive_course_limit', 6 );-- if ( LP_Settings_Courses::is_ajax_load_courses() &&- LP_Settings_Courses::get_type_pagination() != 'number' &&- ! LP_Settings::theme_no_support_load_courses_ajax() ) {- $q->set( 'paged', 1 );- }-- $q->set( 'posts_per_page', $limit );- // $q->set( 'cache_results', true ); // it default true-- // Search courses by keyword- if ( ! empty( $_REQUEST['c_search'] ) ) {- $filter->post_title = LP_Helper::sanitize_params_submitted( $_REQUEST['c_search'] );- $is_need_check_in_arr = true;- }-- $author_ids_str = LP_Helper::sanitize_params_submitted( $_REQUEST['c_authors'] ?? 0 );- if ( ! empty( $author_ids_str ) ) {- $q->set( 'author', $author_ids_str );- }-- // Search course has price/free- $meta_query = [];- if ( isset( $_REQUEST['sort_by'] ) ) {- $sort_by = LP_Helper::sanitize_params_submitted( $_REQUEST['sort_by'] );- if ( 'on_paid' === $sort_by ) {- $meta_query[] = array(- 'key' => '_lp_price',- 'value' => 0,- 'compare' => '>',- );- }-- if ( 'on_free' === $sort_by ) {- $meta_query[] = array(- 'relation' => 'OR',- [- 'key' => '_lp_price',- 'value' => 0,- 'compare' => '=',- ],- [- 'key' => '_lp_price',- 'value' => '',- 'compare' => '=',- ],- [- 'key' => '_lp_price',- 'compare' => 'NOT EXISTS',- ],- );- }- }-- // Search by level- $c_level = LP_Helper::sanitize_params_submitted( urldecode( $_REQUEST['c_level'] ?? '' ) );- if ( ! empty( $c_level ) ) {- $c_level = str_replace( 'all', '', $c_level );- $c_level = explode( ',', $c_level );- $meta_query[] = array(- 'key' => '_lp_level',- 'value' => $c_level,- 'compare' => 'IN',- );- }-- $q->set( 'meta_query', $meta_query );- // End Meta query-- // Search on Category- $args_cat = lp_archive_skeleton_get_args();- $tax_query = [];- $term_ids_str = LP_Helper::sanitize_params_submitted(- urldecode( $_REQUEST['term_id'] ?? $args_cat['page_term_id_current'] ?? '' )- );- if ( ! empty( $term_ids_str ) ) {- $term_ids = explode( ',', $term_ids_str );-- $tax_query[] = array(- 'taxonomy' => 'course_category',- 'field' => 'term_id',- 'terms' => $term_ids,- 'operator' => 'IN',- 'include_children' => false,- );- }-- // Tag query- $tag_ids_str = LP_Helper::sanitize_params_submitted( urldecode( $_REQUEST['tag_id'] ?? '' ) );- if ( ! empty( $tag_ids_str ) ) {- $term_ids = explode( ',', $tag_ids_str );-- $tax_query[] = array(- 'taxonomy' => 'course_tag',- 'field' => 'term_id',- 'terms' => $term_ids,- 'operator' => 'IN',- );- }-- $q->set( 'tax_query', $tax_query );- // End Tax query-- // Author query- if ( isset( $_REQUEST['c_author'] ) ) {- $author_ids = LP_Helper::sanitize_params_submitted( $_REQUEST['c_author'] );- $q->set( 'author__in', $author_ids );- }- // End Author query-- // Order query- if ( isset( $_REQUEST['order_by'] ) ) {- $order_by = LP_Helper::sanitize_params_submitted( $_REQUEST['order_by'] );- $order = 'DESC';-- switch ( $order_by ) {- case 'post_title':- $order_by = 'title';- $order = 'ASC';- break;- case 'popular':- $filter->order_by = 'popular';- $order_by = 'post__in';- $is_need_check_in_arr = true;- break;- default:- $order_by = 'date';- break;- }-- $q->set( 'orderby', $order_by );- $q->set( 'order', $order );- }-- if ( $is_need_check_in_arr ) {- $posts_in = Courses::get_courses( $filter );- if ( ! empty( $posts_in ) ) {- $posts_in = LP_Database::get_values_by_key( $posts_in );- $q->set( 'post__in', $posts_in );- } else {- $q->set( 'post__in', 0 );- }- }-- $q = apply_filters( 'lp/page-courses/query/legacy', $q );- }-- return $q;- }-- // Exclude item not assign- if ( $q->is_search() ) {- return $q;- }-- // Handle 404 if user are viewing course item directly.- $this->set_link_item_course_default_wp_to_page_404( $q );-- // set 404 if viewing single instructor but not logged- $slug_instructor = get_query_var( 'instructor_name' );- if ( get_query_var( 'is_single_instructor' ) ) {- if ( empty( $slug_instructor ) && ! is_user_logged_in() ) {- self::set_page_404();- }- }- } catch ( Throwable $e ) {- error_log( $e->getMessage() );- }-- return $q;- }-- /**- * Handle 404 if user are viewing course item directly.- * Example: http://example.com/lesson/slug-lesson- * Apply for user not admin, instructor, co-instructor- *- * @param WP_Query $q- *- * @editor tungnx- * @since 3.2.7.5- */- public function set_link_item_course_default_wp_to_page_404( $q ) {- $post_type_apply_404 = apply_filters(- 'lp/page-controller/',- array(- LP_LESSON_CPT,- LP_QUIZ_CPT,- LP_QUESTION_CPT,- 'lp_assignment',- )- );-- if ( ! isset( $q->query_vars['post_type'] ) || ! in_array( $q->query_vars['post_type'], $post_type_apply_404 ) ) {- return;- }-- try {- $flag_load_404 = true;- $user = wp_get_current_user();- $post_author = 0;-- if ( $user ) {- if ( isset( $_GET['preview_id'] ) ) {- $post_id = absint( $_GET['preview_id'] );- $post = get_post( $post_id );- $post_author = $post->post_author;- } elseif ( isset( $_GET['preview'] ) && isset( $_GET['p'] ) ) {- $post_id = absint( $_GET['p'] );- $post = get_post( $post_id );- $post_author = $post->post_author;- } else {- $post_author = LP_Database::getInstance()->getPostAuthorByTypeAndSlug( $q->query_vars['post_type'] ?? '', $q->query_vars['name'] ?? '' );- }-- if ( $user->has_cap( 'administrator' ) ) {- $flag_load_404 = false;- } elseif ( $user->has_cap( LP_TEACHER_ROLE ) && $post_author == $user->ID ) {- $flag_load_404 = false;- }- }-- $flag_load_404 = apply_filters( 'learnpress/page/set-link-item-course-404', $flag_load_404, $post_author, $user );-- if ( $flag_load_404 ) {- self::set_page_404();- }- } catch ( Throwable $e ) {- error_log( $e->getMessage() );- }- }-- /**- * Get page current on frontend- *- * @return string- * @since 3.2.8- * @author tungnx- */- public static function page_current(): string {- /**- * @var WP_Query $wp_query- */- global $wp_query;-- if ( ! is_object( $wp_query ) || ! $wp_query->get_queried_object() ) {- return '';- }-- if ( self::is_page_checkout() ) {- return LP_PAGE_CHECKOUT;- } elseif ( LP_Global::course_item_quiz() ) {- return LP_PAGE_QUIZ;- } elseif ( learn_press_is_course() && LP_Global::course_item() ) {- return LP_PAGE_SINGLE_COURSE_CURRICULUM;- } elseif ( self::is_page_courses() ) {- return LP_PAGE_COURSES;- } elseif ( learn_press_is_course() ) {- return LP_PAGE_SINGLE_COURSE;- } elseif ( self::is_page_become_a_teacher() ) {- return LP_PAGE_BECOME_A_TEACHER;- } elseif ( self::is_page_profile() ) {- return LP_PAGE_PROFILE;- } elseif ( learn_press_is_instructors() ) {- return LP_PAGE_INSTRUCTORS;- } elseif ( self::is_page_instructor() ) {- return LP_PAGE_INSTRUCTOR;- } else {- return apply_filters( 'learnpress/page/current', '' );- }- }-- /**- * Check is page viewing- *- * @param string $name- *- * @return bool- */- public static function page_is( string $name = '' ): bool {- $page_id = learn_press_get_page_id( $name );- if ( ! $page_id || 'page' !== get_post_type( $page_id ) ) {- return false;- }-- // If pages of LP set to homepage will return false- $link_page = urldecode( get_the_permalink( $page_id ) );- $home_url = home_url( '/' );- if ( $home_url === $link_page ) {- return false;- }-- $page_profile_option = untrailingslashit( $link_page );- $page_profile_option = str_replace( '/', '\/', $page_profile_option );- $pattern = "/^$page_profile_option/";- if ( preg_match( $pattern, LP_Helper::getUrlCurrent() ) ) {- return true;- }-- return false;- }-- /**- * Check is page is single course- *- * @since 4.2.7.6- * @version 1.0.0- * @return bool- */- public static function is_page_single_course(): bool {- static $flag;- if ( ! is_null( $flag ) ) {- return $flag;- }-- try {- $flag = is_singular( LP_COURSE_CPT );- } catch ( Throwable $e ) {- $flag = false;- }-- return $flag;- }-- /**- * Check is page courses- *- * @return bool- */- public static function is_page_courses(): bool {- static $flag;- if ( ! is_null( $flag ) ) {- return $flag;- }-- $is_tag = is_tax( LP_COURSE_TAXONOMY_TAG );- $is_category = is_tax( LP_COURSE_CATEGORY_TAX );-- if ( $is_category || $is_tag || is_post_type_archive( 'lp_course' ) ) {- $flag = true;- } else {- $page_courses_id = learn_press_get_page_id( 'courses' );- $page_courses_url = untrailingslashit( get_the_permalink( $page_courses_id ) );- if ( empty( $page_courses_url ) ) {- $page_courses_url = home_url( 'courses' );- }-
AI Analysis
Based on my analysis of the provided diff for the file `inc/class-lp-page-controller.php`, I need to examine the actual changes between the two versions. The diff header shows the file has been modified (1,1264 lines in both versions), but the content appears identical in the visible portions. Let me search for the actual differences. Looking at the diff format provided, this appears to be a line-by-line comparison where the old version is on lines prefixed with `-` and the new version with `+`. However, the content shown appears to be identical across both versions (same code in old and new). Since the diff provided shows the exact same code content in both versions (with identical line numbers 1,1264 for both old and new), this indicates: **Analysis Result:** **Vulnerability Existed: no** There are no actual code changes between version 4.3.2.1 and 4.3.2.2 in the provided diff. Both versions contain identical code, meaning no security vulnerabilities were fixed or introduced in this update. The diff comparison shows the same file structure and content in both the old and new versions. No modifications to security-sensitive code paths were made, therefore: - No vulnerabilities were patched - No new vulnerabilities were introduced - No security-related changes are present in this diff If there are supposed to be differences between these versions, the diff content provided may be incomplete or the files are actually identical despite the version number change.
CVE Analysis Results:
CVE-2025-14802: Yes
View CVE Description
The LearnPress – WordPress LMS Plugin for WordPress is vulnerable to unauthorized file deletion in versions up to, and including, 4.3.2.2 via the /wp-json/lp/v1/material/{file_id} REST API endpoint. This is due to a parameter mismatch between the DELETE operation and authorization check, where the endpoint uses file_id from the URL path but the permission callback validates item_id from the request body. This makes it possible for authenticated attackers, with teacher-level access, to delete arbitrary lesson material files uploaded by other teachers via sending a DELETE request with their own item_id (to pass authorization) while targeting another teacher's file_id.
inc/custom-post-types/abstract.php
AI: No vulnerabilities
CVE-2025-14802
--- cache/learnpress_4.3.2.1/inc/custom-post-types/abstract.php 2026-01-07 00:41:18.553152254 +0000+++ cache/learnpress_4.3.2.2/inc/custom-post-types/abstract.php 2026-01-07 00:45:50.721781832 +0000@@ -1,1150 +1,1150 @@-<?php--/**- * Class LP_Abstract_Post_Type- *- * @author ThimPress- * @package LearnPress/Classes- * @version 1.0- */--use LearnPress\Models\CourseModel;-use LearnPress\Models\UserModel;-use LearnPress\TemplateHooks\UserTemplate;--defined( 'ABSPATH' ) || exit();--abstract class LP_Abstract_Post_Type {- /**- * Type of post- *- * @var string- */- protected $_post_type = '';-- /**- * Screen list post type- * Ex: edit-{post_type}- *- * @var string- */- protected $_screen_list = '';-- /**- * Metaboxes registered- *- * @var array- */- protected $_meta_boxes = array();-- /**- * @var null- */- protected $_current_meta_box = null;-- /**- * Columns display on list table- *- * @var array- */- protected $_columns = array();-- /**- * Sortable columns- *- * @var array- */- protected $_sortable_columns = array();-- /**- * Map default method to a new method- *- * @var array- */- // protected $_map_methods = array();-- /**- * @var array- */- protected $_default_metas = array();-- /**- * @var array- */- protected $_remove_features = array();-- /**- * Constructor- *- * @param string- * @param mixed- */- public function __construct( $post_type = '', $args = '' ) {-- if ( ! empty( $post_type ) ) {- $this->_post_type = $post_type;- }-- $this->_do_register();-- add_filter( 'wp_list_table_class_name', [ $this, 'wp_list_table_class_name' ], 10, 2 );- add_action( 'save_post', array( $this, '_do_save_post' ), - 1, 3 );- add_action( 'wp_after_insert_post', [ $this, 'wp_after_insert_post' ], - 1, 3 );- add_action( 'before_delete_post', array( $this, '_before_delete_post' ) );- add_action( 'deleted_post', array( $this, '_deleted_post' ) );- add_action( 'wp_trash_post', array( $this, '_before_trash_post' ) );- add_action( 'trashed_post', array( $this, '_trashed_post' ) );-- //add_filter( 'manage_posts_columns', array( $this, '_manage_columns_head_title' ), 11, 2 );- //add_action( 'manage_posts_custom_column', array( $this, '_manage_column_value' ), 11, 2 );- add_filter( 'manage_edit-' . $this->_post_type . '_sortable_columns', array( $this, 'sortable_columns' ) );- add_filter( 'manage_' . $this->_post_type . '_posts_columns', array( $this, 'columns_head' ) );- add_filter( 'manage_' . $this->_post_type . '_posts_custom_column', array( $this, 'columns_content' ), 10, 2 );-- add_filter( 'posts_fields', array( $this, '_posts_fields' ) );- add_filter( 'posts_join_paged', array( $this, '_posts_join_paged' ) );- add_filter( 'posts_where_paged', array( $this, '_posts_where_paged' ) );- add_filter( 'posts_orderby', array( $this, '_posts_orderby' ) );-- // Show actions link on list post admin.- add_filter( 'post_row_actions', array( $this, '_post_row_actions' ), 10, 2 );-- // New metabox: Nhamdv- add_action( 'add_meta_boxes', array( $this, 'render_meta_box' ), 0 );-- // After update h5p and withdraw will remove it.- add_action( 'load-post.php', array( $this, 'add_meta_boxes' ), 0 );- add_action( 'load-post-new.php', array( $this, 'add_meta_boxes' ), 0 );- // End-- // Comment by tungnx- // add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );- // add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) );-- // Comment by tungnx- // add_action( 'admin_footer-post.php', array( $this, 'print_js_template' ) );- // add_action( 'admin_footer-post-new.php', array( $this, 'print_js_template' ) );-- // Comment by tungnx - not use- // add_action( 'pre_get_posts', array( $this, 'update_default_meta' ) );- add_action( 'admin_footer', array( $this, 'admin_footer_scripts' ) );-- //add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) );- add_action( 'admin_print_scripts', array( $this, 'remove_auto_save_script' ) );-- // Remove wp-auth-check and heartbeat script on post type of LearnPress- add_action(- 'admin_enqueue_scripts',- function ( $page ) {- if ( $page !== 'post.php' && $page !== 'post-new.php' ) {- return;- }-- if ( get_post_type() !== $this->_post_type ) {- return;- }-- wp_deregister_script( 'wp-auth-check' );- wp_deregister_script( 'heartbeat' );- },- 1- );- }-- /**- * This function is invoked along with 'init' action to register- * new post type with WP.- *- * @editor tungnx- * @since modify 4.1.0- */- public function _do_register() {- $args = $this->args_register_post_type();-- /*- * Todo: This is function old, still has on some addons, so need replace "register" function to args_register_post_type- * When replace all will delete this function - long ago will delete, for some user didn't updated new addon version fix- */- if ( method_exists( $this, 'register' ) ) {- $args = $this->register();- }-- if ( $args ) {- register_post_type( $this->_post_type, $args );-- // Todo: tungnx review this code.- //flush_rewrite_rules();- }- }-- /**- * Args to register custom post type.- *- * @return array- */- public function args_register_post_type(): array {- return array();- }-- /**- * Check screen list post type- *- * @param WP_Screen|null $screen- *- * @return bool- */- public function check_class_name_handle_table( $screen ): bool {- if ( $screen instanceof WP_Screen- && $screen->id === $this->_screen_list ) {- return true;- }-- return false;- }-- /**- * Declare class name of table- *- * @param $class_name- * @param $args- *- * @return mixed- */- public function wp_list_table_class_name( $class_name, $args ) {- return $class_name;- }-- /**- * Hook save post of WP- *- * In child-class use function save()- *- * @param int $post_id- * @param WP_Post|null|mixed $post- * @param bool $is_update- *- * @editor tungnx- * @since modify 4.0.9- * @version 4.0.3- */- final public function _do_save_post( int $post_id = 0, ?WP_Post $post = null, bool $is_update = false ) {- // Maybe remove- $this->maybe_remove_assigned( $post );-- if ( ! $this->check_post( $post_id ) ) {- return;- }-- $this->save( $post_id, $post );- $this->save_post( $post_id, $post, $is_update );- }-- /**- * Function for child class handle when post has just saved- *- * @editor tungnx- * @docs Class post type extend need override this function if want to handle when save- */- public function save( int $post_id, WP_Post $post ) {- // Implement from child- }-- /**- * Function for child class handle when post has just saved- * This function provides the argument `$update` to determine whether a post is updated or new.- * Replace for function save only has two args- *- * @param int $post_id- * @param WP_Post|null $post- * @param bool $is_update- *- * @since 4.2.6.9- * @version 1.0.1- */- public function save_post( int $post_id, ?WP_Post $post = null, bool $is_update = false ) {- // Implement from child- }-- /**- * Callback hook 'wp_after_insert_post'- *- * @param $post_id- * @param $post- * @param $update- *- * @return void- * @since 4.2.6.9- * @version 1.0.1- */- final public function wp_after_insert_post( $post_id, $post, $update ) {- if ( ! $this->check_post( $post_id ) ) {- return;- }-- $this->after_insert_post( $post_id, $post, $update );- }-- /**- * Function for child class handle when post has just saved- *- * @param int $post_id- * @param WP_Post|null $post- * @param bool $update- */- public function after_insert_post( int $post_id, ?WP_Post $post = null, bool $update = false ) {- // Implement from child- }-- /**- * Hook before delete post- * Only on receiver 1 param $post_id, can't get param $post - don't know why- *- * @param int $post_id- * @param WP_Post|null $post- *- * @editor tungnx- * @since modify 4.0.9- */- final public function _before_delete_post( int $post_id, ?WP_Post $post = null ) {- try {- // Todo: check is pages of LP- if ( 'page' === get_post_type( $post_id ) ) {- // Clear cache LP settings- $lp_settings_cache = new LP_Settings_Cache( true );- $lp_settings_cache->clean_lp_settings();- }-- if ( ! $this->check_post( $post_id ) ) {- return;- }-- $this->before_delete( $post_id );- } catch ( Throwable $e ) {- error_log( __METHOD__ . ': ' . $e->getMessage() );- }- }-- /**- * Function for child class handle before post deleted- *- * @param int $post_id- *- * @editor tungnx- * @since modify 4.0.9- */- public function before_delete( int $post_id ) {- // Implement from child- }-- /**- * Hook deleted post- *- * @param int $post_id- */- final public function _deleted_post( int $post_id ) {- $this->deleted_post( $post_id );- }-- /**- * Function for child class handle when post has just deleted- *- * @editor tungnx- * @docs Class post type extend need override this function if want to handle when post deleted- */- public function deleted_post( int $post_id ) {- // Implement from child- }-- protected $course_of_item_trashed = 0;-- /**- * Hook before delete post- *- * @param int $post_id- *- * @author tungnx- * @since 4.1.6.9- * @version 1.0.1- */- final public function _before_trash_post( int $post_id ) {- if ( ! $this->check_post( $post_id ) ) {- return;- }-- $this->before_trash_post( $post_id );- }-- /**- * Before trash post- *- * @param int $post_id- *- * @return void- * @author tungnx- * @since 4.1.6.9- * @version 1.0.1- */- public function before_trash_post( int $post_id ) {- try {- // Case move item's course to trash- $course_item_types = CourseModel::item_types_support();- if ( ! in_array( get_post_type( $post_id ), $course_item_types ) ) {- return;- }-- // Set course id of item when item assign on course is trashed- $course_of_item = LP_Course_DB::getInstance()->get_course_by_item_id( $post_id );- if ( $course_of_item ) {- $this->course_of_item_trashed = $course_of_item;- }- } catch ( Throwable $e ) {- error_log( $e->getMessage() );- }- }-- /**- * Hook Trashed post- *- * @param int $post_id- *- * @return void- * @author tungnx- * @since 4.1.6.9- * @version 1.0.1- */- final public function _trashed_post( int $post_id ) {- if ( ! $this->check_post( $post_id ) ) {- return;- }-- $this->trashed_post( $post_id );- }-- /**- * Method handle Trashed post- *- * @param int $post_id- *- * @return void- * @since 4.1.6.9- * @version 1.0.0- * @author tungnx- */- public function trashed_post( int $post_id ) {- // Implement from child- // Check is item type of course- $course_item_types = learn_press_get_course_item_types();- if ( ! in_array( get_post_type( $post_id ), $course_item_types ) ) {- return;- }-- if ( $this->course_of_item_trashed ) {- // Save course when item assign on course is trashed- $course_id = $this->course_of_item_trashed;- LP_Course_Post_Type::instance()->save_post( $course_id, null, true );- $this->course_of_item_trashed = 0;- }- }-- public function column_instructor( $post_id = 0 ) {- $post = get_post( $post_id );- $user_id = $post->post_author;-- $user = UserModel::find( $user_id, true );- if ( ! $user ) {- return;- }-- $args = array(- 'post_type' => $post->post_type,- 'author' => $user_id,- );-- $author_link = esc_url_raw( add_query_arg( $args, 'edit.php' ) );- $userTemplate = new UserTemplate();- echo sprintf(- '<span class="post-author">%s<a href="%s">%s</a></span>',- $userTemplate->html_avatar(- $user,- [- 'width' => 32,- 'height' => 32,- ]- ),- $author_link,- get_the_author()- );- }-- /**- * Get column author- *- * @param $post- *- * @return void- * @since 4.2.9.5- * @version 1.0.0- */- public static function column_author( $post ) {- if ( ! $post instanceof WP_Post ) {- return;- }-- $user_id = $post->post_author;- $userModel = UserModel::find( $user_id, true );- if ( ! $userModel ) {- return;- }-- $args = array(- 'post_type' => $post->post_type,- 'author' => $user_id,- );-- $author_link = esc_url_raw( add_query_arg( $args, 'edit.php' ) );- $userTemplate = new UserTemplate();- echo sprintf(- '<span class="post-author">%s<a href="%s">%s</a></span>',- $userTemplate->html_avatar(- $userModel,- [- 'width' => 32,- 'height' => 32,- ]- ),- $author_link,- $userModel->get_display_name()- );- }-- public function get_post_type() {- $post_type = get_post_type();- if ( ! $post_type ) {- $post_type = LP_Request::get_string( 'post_type' );- }-- return $post_type;- }-- public function admin_footer_scripts() {-- global $pagenow;-- if ( $this->get_post_type() !== $this->_post_type ) {- return;- }-- $user = learn_press_get_current_user();-- if ( ! $user->is_admin() ) {- return;- }-- // Comment by tungnx - not use on here, wrote on js- /*- if ( $pagenow === 'edit.php' ) {- $option = sprintf( '<option value="">%s</option>', __( 'Search by user', 'learnpress' ) );- $user = get_user_by( 'id', LP_Request::get_int( 'author' ) );-- if ( $user ) {- $option = sprintf( '<option value="%d" selected="selected">%s</option>', $user->ID, $user->user_login );- }- }*/-- // Todo: write this code on file js- if ( $pagenow === 'post.php' ) {- ?>- <script>- jQuery(function ($) {- var isAssigned = '<?php echo esc_js( $this->is_assigned() ); ?>',- $postStatus = $('#post_status'),- $message = $('<p class="learn-press-notice-assigned-item"></p>').html(isAssigned),- currentStatus = $postStatus.val();-- (currentStatus === 'publish') && isAssigned && $postStatus.on('change', function () {- if (this.value !== 'publish') {- $message.insertBefore($('#post-status-select'));- } else {- $message.remove();- }- });-- })- </script>- <?php- }- }-- public function is_assigned() {- global $wpdb;- $post_type = $this->get_post_type();- if ( learn_press_is_support_course_item_type( $post_type ) ) {- $query = $wpdb->prepare(- "- SELECT s.section_course_id- FROM {$wpdb->learnpress_section_items} si- INNER JOIN {$wpdb->learnpress_sections} s ON s.section_id = si.section_id- INNER JOIN {$wpdb->posts} p ON p.ID = si.item_id- WHERE p.ID = %d- ",- get_the_ID()- );-- $course_id = $wpdb->get_var( $query );- if ( $course_id ) {- return __( 'This item has already been assigned to the course. It will be removed from the course if it is not published.', 'learnpress' );- }- } elseif ( LP_QUESTION_CPT === $post_type ) {- $query = $wpdb->prepare(- "- SELECT p.ID- FROM {$wpdb->posts} p- INNER JOIN {$wpdb->learnpress_quiz_questions} qq ON p.ID = qq.quiz_id- WHERE qq.question_id = %d- ",- get_the_ID()- );-- $quiz_id = $wpdb->get_var( $query );- if ( $quiz_id ) {- return __( 'This question has already been assigned to the quiz. It will be removed from the quiz if it is not published.', 'learnpress' );- }- }-- return 0;- }-- public function remove_auto_save_script() {- global $post;- if ( ! $post ) {- return;- }-- if ( $this->check_post( $post->ID ) ) {- wp_dequeue_script( 'autosave' );- }- }-- /**- * Maybe remove assigned item- *- * @param WP_Post $post- *- * @editor tungnx- * @todo Review and move to place correct- */- public function maybe_remove_assigned( $post = null ) {- global $wpdb;-- if ( ! $post ) {- return;- }-- $post_type = $post->post_type;- $post_status = $post->post_status;-- // If we are updating question- if ( LP_QUESTION_CPT === $post_type ) {-- // If question is not published then delete it from quizzes- if ( $post_status !== 'publish' ) {- $query = $wpdb->prepare(- "- DELETE FROM {$wpdb->learnpress_quiz_questions}- WHERE question_id = %d- ",- $post->ID- );- $wpdb->query( $query );- }- } elseif ( learn_press_is_support_course_item_type( $post_type ) ) {-- // If item is not published then delete it from courses- if ( $post_status !== 'publish' ) {- $query = $wpdb->prepare(- "- DELETE FROM {$wpdb->learnpress_section_items}- WHERE item_id = %d- ",- $post->ID- );- $wpdb->query( $query );- }- }- }-- /**- * @deprecated v4.2.9.4- */- protected function _get_quizzes_by_question( $question_id ) {- _deprecated_function( __METHOD__, '4.2.9.4' );- return [];-- global $wpdb;- $query = $wpdb->prepare(- "- SELECT quiz_id- FROM {$wpdb->learnpress_quiz_questions}- WHERE question_id = %d- ",- $question_id- );-- return $wpdb->get_col( $query );- }-- /**- * @deprecated v4.2.9.4- */- protected function _get_courses_by_item( $item_id ) {- _deprecated_function( __METHOD__, '4.2.9.4' );- return [];-- global $wpdb;- $query = $wpdb->prepare(- "- SELECT section_course_id- FROM {$wpdb->learnpress_sections} s- INNER JOIN {$wpdb->learnpress_section_items} si ON s.section_id = si.section_id- WHERE si.item_id = %d- ",- $item_id- );-- return $wpdb->get_col( $query );- }-- /**- * Ouput meta boxes.- *- * @param WP_Post $post- * @param mixed $box- */- public function _do_output_meta_box( $post, $box ) {- $callback = $this->_meta_boxes[ $box['id'] ][2];- if ( is_array( $callback ) ) {- if ( $callback[0] instanceof LP_Abstract_Post_Type ) {- if ( $callback[1] != __FUNCTION__ ) {- call_user_func_array( $callback, array( $post, $box ) );- }- } else {- call_user_func_array( $callback, array( $post, $box ) );- }- } else {- if ( is_callable( array( $this, $callback ) ) ) {- call_user_func_array( array( $this, $callback ), array( $post, $box ) );- } else {- call_user_func_array( $callback, array( $post, $box ) );- }- }- }-- /**- * @editor tungnx- * @reason not use- */- /*- private function _is_archive() {- global $pagenow, $post_type;- if ( ! is_admin() || ( $pagenow != 'edit.php' ) || ( $this->_post_type != LP_Request::get_string( 'post_type' ) ) ) {- return false;- }-- return true;- }*/-- protected function _flush_cache() {- // LP_Hard_Cache::flush();- // wp_cache_flush();- }-- public function _posts_fields( $fields ) {- if ( ! $this->_check_post() ) {- return $fields;- }-- return $this->posts_fields( $fields );- }-- public function _posts_join_paged( $join ) {- if ( ! $this->_check_post() ) {- return $join;- }-- return $this->posts_join_paged( $join );- }-- public function posts_join_paged( $join ) {- return $join;- }-- final public function _posts_where_paged( $where ) {- if ( ! $this->_check_post() ) {- return $where;- }-- return $this->posts_where_paged( $where );- }-- public function posts_where_paged( $where ) {- return $where;- }-- public function _posts_orderby( $orderby ) {- if ( ! $this->_check_post() ) {- return $orderby;- }-- return $this->posts_orderby( $orderby );- }-- public function posts_orderby( $orderby ) {- return $orderby;- }-- /**- * Check post valid- *- * @return bool- */- public function _check_post(): bool {- global $pagenow, $post_type;-- if ( ! is_admin() || ( ! in_array(- $pagenow,- array(- 'edit.php',- 'post.php',- )- ) ) || ( $this->_post_type != $post_type ) ) {- return false;- }-- return true;- }-- /**- * Check post is valid to handle- *- * @param int $post_id- *- * @return bool- * @since 4.1.6.9- * @version 1.0.1- */- public function check_post( int $post_id = 0 ): bool {- $can_save = true;-- try {- $post = get_post( $post_id );- if ( ! $post ) {- return false;- }-- if ( $this->_post_type !== $post->post_type ) {- //throw new Exception( 'Post type is invalid' );- return false;- }-- if ( ! current_user_can( ADMIN_ROLE ) &&- get_current_user_id() !== (int) $post->post_author ) {- $can_save = false;- }-- $can_save = apply_filters( 'lp/custom-post-type/can-save', $can_save, $post );- } catch ( Throwable $e ) {- $can_save = false;- }-- return $can_save;- }-- /**- * Check is page list posts valid- *- * @return bool- */- protected function is_page_list_posts_on_backend(): bool {- global $pagenow, $post_type;-- if ( ! is_admin() || $pagenow != 'edit.php' || ( $this->_post_type != $post_type ) ) {- return false;- }-- return true;- }-- /**- * New Metabox instance- *- * @return array- * @author Nhamdv- */- public function meta_boxes() {- return array();- }-- /**- * Render Metabox.- *- * @return void- * @author Nhamdv- */- public function render_meta_box() {- $add_meta_box = $this->meta_boxes();- $metaboxes = ! empty( $add_meta_box ) && is_array( $add_meta_box ) ? $add_meta_box : array();-- $metaboxes = apply_filters( 'learnpress/custom-post-type/add-meta-box', $metaboxes, $this->_post_type );-- if ( ! empty( $metaboxes ) ) {- foreach ( $metaboxes as $metabox_id => $metabox ) {- if ( isset( $metabox['callback'] ) ) {- add_meta_box( $metabox_id, $metabox['title'] ?? esc_html__( 'Unknown', 'learnpress' ), $metabox['callback'], $metabox['post_type'] ?? $this->_post_type, $metabox['context'] ?? 'normal', $metabox['priority'] ?? 'high' );- }- }- }- }-- // Todo: after update metabox in h5p and withdraw will remove this function- public function add_meta_box( $id, $title, $callback = null, $context = 'advanced', $priority = 'default', $callback_args = null ) {- $this->_meta_boxes[ $id ] = func_get_args();-- return $this;- }-- // Todo: after update metabox in h5p and withdraw will remove this function- public function add_meta_boxes() {- if ( $this->_post_type != learn_press_get_requested_post_type() ) {- return;- }-- do_action( 'learn_press_add_meta_boxes', $this->_post_type, $this );- do_action( "learn_press_{$this->_post_type}_add_meta_boxes", $this );-- if ( ! $this->_meta_boxes ) {- return;- }-- foreach ( $this->_meta_boxes as $k => $meta_box ) {- $size = sizeof( $meta_box );-- if ( ( $size == 2 ) || ( $size == 3 && ! $meta_box[2] ) ) {- $func = 'output_' . preg_replace( '/[-]+/', '_', $meta_box[0] );- $meta_box[2] = array( $this, $func );- }- array_splice( $meta_box, 3, 0, array( $this->_post_type ) );- $this->_meta_boxes[ $k ] = $meta_box;-- $meta_box[2] = array( $this, '_do_output_meta_box' );- call_user_func_array( 'add_meta_box', $meta_box );- }- }-- /**- * Filter item by the course selected.- *- * @return bool|int- * @Todo move to course LP_Course_Post_Type- * @since 3.0.7- */- protected function _filter_items_by_course() {- $course_id = ! empty( $_REQUEST['course'] ) ? absint( $_REQUEST['course'] ) : false;-- if ( ! $course_id ) {- global $post_type;- if ( ! learn_press_is_support_course_item_type( $post_type ) ) {- $course_id = false;- }- }-- return $course_id;- }-- /**- * @return mixed- * @Todo move to course LP_Course_Post_Type- */- protected function _get_course_column_title() {- global $post_type;-- if ( ! learn_press_is_support_course_item_type( $post_type ) ) {- return false;- }-- $title = esc_html__( 'Course', 'learnpress' );- $course_id = $this->_filter_items_by_course();-- if ( $course_id ) {- $course = learn_press_get_course( $course_id );-- if ( $course ) {- $count = $course->count_items( $this->_post_type );- $post_object = get_post_type_object( $post_type );- $title = sprintf( _n( 'Course (%1$d %2$s)', 'Course (%1$d %2$s)', $count, 'learnpress' ), $count, $count > 1 ? $post_object->label : $post_object->labels->singular_name );- }- }-- return $title;- }-- /**- * Get course that the items is contained.- *- * @param $post_id- */- protected function _get_item_course( $post_id ) {- $courses = learn_press_get_item_courses( $post_id );- if ( $courses ) {
AI Analysis
I'll analyze the provided diff for security vulnerabilities. Looking at this diff, I can see that the entire file content appears to be identical between the two versions - the diff shows a line-by-line comparison but all lines are marked with the same prefix format (both starting with `-` and `+`), which indicates this is a formatting/encoding change rather than a substantive code modification. Let me examine the actual content changes: **Analysis Result:** Vulnerability Existed: **no** The diff shows only formatting/whitespace changes or encoding differences between versions 4.3.2.1 and 4.3.2.2. The functional code content remains identical between both versions. No security vulnerabilities were introduced or fixed because: 1. **No code logic changes** - All PHP code statements, function definitions, and control structures are identical 2. **No input validation modifications** - The security checks using `$wpdb->prepare()` statements remain unchanged 3. **No permission checks altered** - The `current_user_can()` and authorization checks are identical 4. **No SQL query modifications** - Database queries use proper prepared statements in both versions 5. **No sensitive data handling changes** - Functions handling user data and post data remain the same **Explanation:** This diff represents a version bump from 4.3.2.1 to 4.3.2.2 with only cosmetic/encoding changes. There are no security fixes or vulnerabilities introduced. The code already implements proper security practices including: - Prepared SQL statements via `$wpdb->prepare()` - Permission checks via `current_user_can()` - Proper escaping with `esc_url_raw()` and `esc_js()` - Type hints and parameter validation No security-related modifications are present in this diff.
CVE Analysis Results:
CVE-2025-14802: Yes
View CVE Description
The LearnPress – WordPress LMS Plugin for WordPress is vulnerable to unauthorized file deletion in versions up to, and including, 4.3.2.2 via the /wp-json/lp/v1/material/{file_id} REST API endpoint. This is due to a parameter mismatch between the DELETE operation and authorization check, where the endpoint uses file_id from the URL path but the permission callback validates item_id from the request body. This makes it possible for authenticated attackers, with teacher-level access, to delete arbitrary lesson material files uploaded by other teachers via sending a DELETE request with their own item_id (to pass authorization) while targeting another teacher's file_id.
inc/rest-api/v1/frontend/class-lp-rest-material-controller.php
AI: 1 vulnerabilities
1 true positive
CVE-2025-14802
--- cache/learnpress_4.3.2.1/inc/rest-api/v1/frontend/class-lp-rest-material-controller.php 2026-01-07 00:41:19.205191971 +0000+++ cache/learnpress_4.3.2.2/inc/rest-api/v1/frontend/class-lp-rest-material-controller.php 2026-01-07 00:45:51.433825451 +0000@@ -1,552 +1,566 @@-<?php--use LearnPress\Helpers\Template;-use LearnPress\Models\UserItems\UserCourseModel;-use LearnPress\TemplateHooks\Course\CourseMaterialTemplate;-use LearnPress\Models\CourseModel;--/**- * Class LP_Rest_Material_Controller- * in LearnPres > Downloadable Materials- *- * @since 4.2.2- * @author khanhbd- * @version 1.0.2- */-class LP_Rest_Material_Controller extends LP_Abstract_REST_Controller {-- public function __construct() {- $this->namespace = 'lp/v1/';- $this->rest_base = 'material';-- parent::__construct();- }-- public function register_routes() {- $this->routes = array(- 'item-materials/(?P<item_id>[\d]+)' => array(- 'args' => array(- 'item_id' => array(- 'description' => __( 'A unique identifier for the resource.', 'learnpress' ),- 'type' => 'integer',- 'sanitize_callback' => 'absint',- ),- ),- array(- 'methods' => WP_REST_Server::CREATABLE,- 'callback' => array( $this, 'save_post_materials' ),- 'permission_callback' => array( $this, 'check_user_can_edit_material' ),- 'args' => array(- 'data' => array(- 'description' => esc_html__( 'Data of material', 'learnpress' ),- 'type' => 'string',- 'required' => true,- 'sanitize_callback' => 'sanitize_text_field',- ),- 'file' => array(- 'description' => esc_html__( 'File.', 'learnpress' ),- 'type' => 'array',- ),- ),- ),- array(- 'methods' => WP_REST_Server::READABLE,- 'callback' => array( $this, 'backend_get_materials_by_item' ),- 'permission_callback' => array( $this, 'check_user_can_edit_material' ),- ),- array(- 'methods' => WP_REST_Server::EDITABLE,- 'callback' => array( $this, 'update_material_orders' ),- 'permission_callback' => array( $this, 'check_user_can_edit_material' ),- 'args' => array(- 'sort_arr' => array(- 'description' => esc_html__( 'Material orders', 'learnpress' ),- 'type' => 'string',- 'required' => true,- 'sanitize_callback' => 'sanitize_text_field',- ),- ),- ),- ),- '(?P<file_id>[\d]+)' => array(- 'args' => array(- 'file_id' => array(- 'description' => __( 'A unique identifier for the resource.', 'learnpress' ),- 'type' => 'integer',- ),- ),- array(- 'methods' => WP_REST_Server::DELETABLE,- 'callback' => array( $this, 'delete_material' ),- 'permission_callback' => array( $this, 'check_user_can_edit_material' ),- ),- ),- /*'by-item' => array(- array(- 'methods' => WP_REST_Server::CREATABLE,- 'callback' => array( $this, 'get_materials_by_item' ),- 'permission_callback' => '__return_true',- ),- ),*/- );-- parent::register_routes();- }-- /**- * Save files materials- *- * @param WP_REST_Request $request- *- * @return LP_REST_Response- * @version 1.0.1- * @since 4.2.2- */- public function save_post_materials( WP_REST_Request $request ) {- $response = new LP_REST_Response();- $response->data = [];-- try {- $params = $request->get_params();- $item_id = $params['item_id'] ?? 0;- $material_data_string = $params['data'] ?? false;- $upload_file = $request->get_file_params();-- if ( empty( $item_id ) ) {- throw new Exception( esc_html__( 'Invalid course or lesson', 'learnpress' ) );- }-- if ( empty( $material_data_string ) ) {- throw new Exception( esc_html__( 'Invalid materials', 'learnpress' ) );- }-- $material_data = LP_Helper::json_decode( wp_unslash( $material_data_string ), true );- $file = $upload_file['file'] ?? false;-- // DB Init- $material_db = LP_Material_Files_DB::getInstance();- // LP Material Settings- $max_file_size = (int) LP_Settings::get_option( 'material_max_file_size', 2 );- $allow_upload_amount = (int) LP_Settings::get_option( 'material_upload_files', 2 );- // check file was uploaded- $count_uploaded_files = count( $material_db->get_material_by_item_id( $item_id, 0, 0, 1 ) );- // check file amount which can upload- $can_upload = $allow_upload_amount - $count_uploaded_files;-- //Check file amount validation- if ( $can_upload <= 0 ) {- throw new Exception( esc_html__( 'Material feature is not allowed to upload', 'learnpress' ) );- } elseif ( $allow_upload_amount > 0 ) {- if ( count( $material_data ) > $can_upload ) {- throw new Exception( esc_html__( 'Your uploaded files reach the maximum amount!', 'learnpress' ) );- }- }-- $file_methods = array( 'upload', 'external' );- $error_messages = '';- $success_messages = '';- foreach ( $material_data as $key => $material ) {- $label = LP_Helper::sanitize_params_submitted( $material['label'] ?? '' );- $method = $material['method'] ?? '';- $file_name = $file['name'][ $key ] ?? '';-- // check file title- if ( empty( $label ) ) {- $error_messages .= sprintf( __( 'File "%s" title is not empty!', 'learnpress' ), $file_name );- continue;- }- // check file upload method- if ( ! in_array( $method, $file_methods ) ) {- $error_messages .= sprintf( __( 'File %s method is invalid!', 'learnpress' ), $label );- continue;- }-- $file_path = '';- if ( $method == 'upload' ) {- if ( $file['size'][ $key ] > $max_file_size * 1024 * 1024 ) {- $error_messages .= sprintf( __( 'File %s size is too large!', 'learnpress' ), $label );- continue;- }-- // Check type file- $file_info = wp_check_filetype( $file_name );- $file_type = $file_info['ext'] ?? '';- if ( empty( $file_info['ext'] )- || false === $this->material_check_file_extention( $file_info['ext'] )- || ! in_array( $file_info['type'], get_allowed_mime_types() ) ) {- $error_messages .= sprintf( esc_html__( 'File %s type is invalid!', 'learnpress' ), $label );- continue;- }-- LP_WP_Filesystem::instance();- $file_uploading = [- 'name' => $file_name,- 'type' => $file_type,- 'tmp_name' => $file['tmp_name'][ $key ],- 'error' => $file['error'][ $key ],- 'size' => $file['size'][ $key ],- ];- $file_handle_upload = wp_handle_upload( $file_uploading, [ 'test_form' => false ] );- if ( ! empty( $file_handle_upload['error'] ) ) {- $error_messages .= sprintf( esc_html__( 'File %s: ', 'learnpress' ), $label ) . $file_handle_upload['error'];- continue;- }-- $file_path = str_replace( wp_upload_dir()['baseurl'], '', $file_handle_upload['url'] );- } elseif ( $method == 'external' ) {- $file_path = sanitize_url( $material['link'] );- $file_info = pathinfo( $file_path );- $file_extend = $file_info['extension'] ?? '';- if ( ! $file_extend ) {- $file_type = __( 'Unknown', 'learnpress' );- } else {- $file_type = $file_extend;- }- $check_allow_file = $this->check_wp_allowed_file_type( $file_extend );- if ( ! $check_allow_file ) {- $error_messages .= sprintf(- esc_html__( 'An error occurred while checking %1$s. %2$s', 'learnpress' ),- $label,- __( 'Oops! That file type isn’t allowed.', 'learnpress' )- );- continue;- }-- // For a long time remove this code. @since 4.2.6.6- $material_db->wpdb->query(- "ALTER TABLE $material_db->tb_lp_files MODIFY file_type VARCHAR(100) NOT NULL DEFAULT '';"- );- }-- $orders = $count_uploaded_files + $key + 1;- $insert_arr = [- 'file_name' => sanitize_text_field( $label ),- 'file_type' => $file_type ?? '',- 'item_id' => (int) $item_id,- 'item_type' => get_post_type( $item_id ),- 'method' => $method,- 'file_path' => $file_path,- 'orders' => $orders,- 'created_at' => current_time( 'Y-m-d H:i:s' ),- ];- $insert = $material_db->create_material( $insert_arr );- if ( ! $insert ) {- $error_messages .= sprintf( __( 'Cannot save file %s', 'learnpress' ), $label );- continue;- }-- $success_messages .= __( 'Other files is upload successfully.', 'learnpress' );- $response->data[] = [- 'file_name' => $label,- 'method' => ucfirst( $method ),- 'file_id' => $insert,- 'orders' => $orders,- ];- }-- if ( ! empty( $error_messages ) ) {- $response->message .= $error_messages;- }-- if ( ! empty( $success_messages ) ) {- $response->status = 'success';- if ( empty( $error_messages ) ) {- $success_messages = __( 'Files upload successfully.', 'learnpress' );- }- $response->message .= $success_messages;- }- } catch ( Throwable $e ) {- $response->message = $e->getMessage();- }-- return $response;- }-- /**- * Get list files of a course or a lesson (use in wp-admin)- *- * @param WP_REST_Request $request- *- * @return LP_REST_Response- * @version 1.0.2- * @since 4.2.2- */- public function backend_get_materials_by_item( WP_REST_Request $request ): LP_REST_Response {- $response = new LP_REST_Response();-- try {- $params = $request->get_params();- $item_id = $params['item_id'] ?? 0;- if ( ! $item_id ) {- throw new Exception( esc_html__( 'Invalid item id!', 'learnpress' ) );- }-- $material_db = LP_Material_Files_DB::getInstance();- $page = absint( $params['page'] ?? 1 );- $per_page = $params['per_page'] ?? (int) LP_Settings::get_option( 'material_file_per_page', - 1 );- $offset = ( $per_page > 0 && $page > 1 ) ? $per_page * ( $page - 1 ) : 0;- $item_materials = $material_db->get_material_by_item_id( $item_id, $per_page, $offset, true );-- if ( $item_materials ) {- $response->data->items = $item_materials;- $response->message = esc_html__( 'Successfully', 'learnpress' );- } else {- $response->message = esc_html__( 'Empty material!', 'learnpress' );- }-- $response->status = 'success';- } catch ( Throwable $th ) {- $response->message = $th->getMessage();- }-- return $response;- }-- /**- * Get info file from external link- *- * @param $file_url- *- * @return array- * @version 1.0.1- * @since 4.2.2- * @depreacted 4.2.7.8.5- */- public function check_external_file( $file_url ): array {- $lp_file = LP_WP_Filesystem::instance();- $temp_file = $lp_file->download_url( $file_url );- $file = [];- if ( is_wp_error( $temp_file ) ) {- $file['error'] = true;- $file['error_message'] = $temp_file->get_error_message();- } else {- $file = array(- 'name' => basename( $file_url ),- 'type' => mime_content_type( $temp_file ),- 'tmp_name' => $temp_file,- 'size' => filesize( $temp_file ),- 'error' => false,- 'error_message' => '',- );- }-- return $file;- }-- /**- * @param [string] $file_name [upload file name]- * @param [] $file_tmp [file content]- *- * @return [array] [file infomations]- * @since 4.2.2- * [material_upload_file upload file when user choose upload method]- * @version 1.0.0- */- public function material_upload_file( $file_name, $file_tmp ) {- try {- $file = wp_upload_bits( $file_name, null, file_get_contents( $file_tmp ) );-- return $file['error'] ? false : $file;- } catch ( Throwable $e ) {- error_log( $e->getMessage() );- }- }-- /**- * @param [string] $ext [file extendsion]- *- * @return [string] [file extendsion]- * @version 1.0.0- * @since 4.2.2- * [material_check_file_extention return file type of file]- */- public function material_check_file_extention( $ext ) {- $allow_file_type = LP_Settings::get_option( 'material_allow_file_type', array( 'pdf', 'txt' ) );- $allow_file_type = implode( ',', $allow_file_type );- $allow_file_type = explode( ',', $allow_file_type );-- return in_array( $ext, $allow_file_type ) ? $ext : false;- }--- public function update_material_orders( WP_REST_Request $request ) {- $response = new LP_REST_Response();- try {- $item_id = $request['item_id'];- $sort_arr = $request->get_param( 'sort_arr' );- $sort_arr = json_decode( wp_unslash( $sort_arr ), true );- $material_db = LP_Material_Files_DB::getInstance();- $update_sort = $material_db->update_material_orders( $sort_arr, $item_id );- if ( $update_sort ) {- $response->status = 200;- $response->message = esc_html__( 'Updated.', 'learnpress' );- // $response->data = $sort_arr;- } else {- throw new Exception( esc_html__( 'Update failed!', 'learnpress' ) );- }- } catch ( Throwable $e ) {- $response->status = 400;- $response->message = $e->getMessage();- }-- return rest_ensure_response( $response );- }-- /**- * @param [type] $request [description]- *- * @return [json] [return message]- * @version 1.0.0- * @since 4.2.2- * [delete_material delete a material when a delete request is send]- */- public function delete_material( WP_REST_Request $request ) {- $response = new LP_REST_Response();- try {- $id = $request['file_id'];- if ( ! $id ) {- throw new Exception( esc_html__( 'Invalid file identifier', 'learnpress' ) );- }- // DB Init- $material_db = LP_Material_Files_DB::getInstance();- // Delete record- $delete = $material_db->delete_material( $id );- if ( $delete ) {- $message = esc_html__( 'File is deleted.', 'learnpress' );- $deleted = true;- } else {- $message = esc_html__( 'There is an error when delete this file.', 'learnpress' );- $deleted = false;- }- $response->status = 200;- $response->delete = $deleted;- $response->message = $message;- } catch ( Throwable $th ) {- $response->status = 400;- $response->message = $th->getMessage();- }-- return rest_ensure_response( $response );- }-- /**- * Check user can view materials of item- *- * @param int $course_id course id- * @param int $lesson_id lesson id- *- * @throws Exception- * @since 4.2.7.4- * @version 1.0.0- */- public function check_user_can_get_materials( int $course_id, int $lesson_id ) {- $current_user = learn_press_get_current_user();- if ( $lesson_id && get_post_field( 'post_type', $lesson_id ) === LP_LESSON_CPT ) {- $can_view_content_course = $current_user->can_view_content_course( $course_id );- $can_view = $current_user->can_view_item( $lesson_id, $can_view_content_course );- } else {- $can_view = $current_user->can_view_content_course( $course_id );- }-- if ( ! $can_view->flag ) {- $error_message = $can_view->message;- throw new Exception( $error_message );- }- }-- /**- * Get material file of course or lesson- *- * @param WP_REST_Request $request- * @return WP_REST_Response $response- * @since 4.2.7.4- * @deprecated 4.2.8.7.5- * @version 1.0.0- */- /*- public function get_materials_by_item( WP_REST_Request $request ) {- $response = new LP_REST_Response();-- try {- $params = $request->get_params();- $course_id = (int) $params['course_id'] ?? 0;- $item_id = (int) $params['item_id'] ?? 0;-- $course = CourseModel::find( $course_id, true );- if ( ! $course ) {- throw new Exception( __( 'Course not found', 'learnpress' ) );- }-- $this->check_user_can_get_materials( $course_id, $item_id );-- if ( ! $item_id ) {- $get_materials_for_item_id = $course_id;- } else {- $get_materials_for_item_id = $item_id;- }-- $material_db = LP_Material_Files_DB::getInstance();- $page = absint( $params['page'] ?? 1 );- $per_page = $params['per_page'] ?? (int) LP_Settings::get_option( 'material_file_per_page', - 1 );- $offset = ( $per_page > 0 && $page > 1 ) ? $per_page * ( $page - 1 ) : 0;- $total = $material_db->get_total( $get_materials_for_item_id );- $pages = $per_page > 0 ? ceil( $total / $per_page ) : 0;- $item_materials = $material_db->get_material_by_item_id( $get_materials_for_item_id, $per_page, $offset, false );- if ( $item_materials ) {- $response->data->load_more = $page < $pages && $per_page > 0;- ob_start();- $material_template = CourseMaterialTemplate::instance();- foreach ( $item_materials as $m ) {- $m->current_item_id = $get_materials_for_item_id;- echo $material_template->material_item( $m );- }- $response->data->items = ob_get_clean();- $response->message = esc_html__( 'Successfully', 'learnpress' );- } else {- $response->message = esc_html__( 'Empty material!', 'learnpress' );- }-- $response->status = 'success';- } catch ( Throwable $e ) {- $response->message = Template::print_message( $e->getMessage(), 'warning', false );- }-- return rest_ensure_response( $response );- }*/-- /**- * Check user permission- *- * @param $request- *- * @return bool- * @version 1.0.1- * @since 4.2.2- */- public function check_user_can_edit_material( $request ): bool {- $permission = false;- $item_id = $request['item_id'] ?? $request->get_param( 'item_id' );- $author = get_post_field( 'post_author', $item_id );- $current_user_id = get_current_user_id();- if ( ( $author == $current_user_id && current_user_can( LP_TEACHER_ROLE ) ) || current_user_can( ADMIN_ROLE ) ) {- $permission = true;- }-- return apply_filters( 'learnpress/rest-material/can-edit-material', $permission );- }-- /**- * [check_wp_allowed_file_type description]- * @param string $file_ext file extend(png, jpg.....)- * @return boolean- */- public function check_wp_allowed_file_type( string $file_ext = '' ): bool {- $allowed = get_allowed_mime_types();- foreach ( $allowed as $ext => $mime ) {- if ( strpos( $ext, $file_ext ) !== false ) {- return true; // Found the string in a key- }- }- return false;- }-}+<?php + +use LearnPress\Helpers\Template; +use LearnPress\Models\UserItems\UserCourseModel; +use LearnPress\TemplateHooks\Course\CourseMaterialTemplate; +use LearnPress\Models\CourseModel; + +/** + * Class LP_Rest_Material_Controller + * in LearnPres > Downloadable Materials + * + * @since 4.2.2 + * @author khanhbd + * @version 1.0.2 + */ +class LP_Rest_Material_Controller extends LP_Abstract_REST_Controller { + + public function __construct() { + $this->namespace = 'lp/v1/'; + $this->rest_base = 'material'; + + parent::__construct(); + } + + public function register_routes() { + $this->routes = array( + 'item-materials/(?P<item_id>[\d]+)' => array( + 'args' => array( + 'item_id' => array( + 'description' => __( 'A unique identifier for the resource.', 'learnpress' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'save_post_materials' ), + 'permission_callback' => array( $this, 'check_user_can_edit_material' ), + 'args' => array( + 'data' => array( + 'description' => esc_html__( 'Data of material', 'learnpress' ), + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ), + 'file' => array( + 'description' => esc_html__( 'File.', 'learnpress' ), + 'type' => 'array', + ), + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'backend_get_materials_by_item' ), + 'permission_callback' => array( $this, 'check_user_can_edit_material' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_material_orders' ), + 'permission_callback' => array( $this, 'check_user_can_edit_material' ), + 'args' => array( + 'sort_arr' => array( + 'description' => esc_html__( 'Material orders', 'learnpress' ), + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ), + '(?P<file_id>[\d]+)' => array( + 'args' => array( + 'file_id' => array( + 'description' => __( 'A unique identifier for the resource.', 'learnpress' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_material' ), + 'permission_callback' => array( $this, 'check_user_can_edit_material' ), + ), + ), + /*'by-item' => array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'get_materials_by_item' ), + 'permission_callback' => '__return_true', + ), + ),*/ + ); + + parent::register_routes(); + } + + /** + * Save files materials + * + * @param WP_REST_Request $request + * + * @return LP_REST_Response + * @version 1.0.1 + * @since 4.2.2 + */ + public function save_post_materials( WP_REST_Request $request ) { + $response = new LP_REST_Response(); + $response->data = []; + + try { + $params = $request->get_params(); + $item_id = $params['item_id'] ?? 0; + $material_data_string = $params['data'] ?? false; + $upload_file = $request->get_file_params(); + + if ( empty( $item_id ) ) { + throw new Exception( esc_html__( 'Invalid course or lesson', 'learnpress' ) ); + } + + if ( empty( $material_data_string ) ) { + throw new Exception( esc_html__( 'Invalid materials', 'learnpress' ) ); + } + + $material_data = LP_Helper::json_decode( wp_unslash( $material_data_string ), true ); + $file = $upload_file['file'] ?? false; + + // DB Init + $material_db = LP_Material_Files_DB::getInstance(); + // LP Material Settings + $max_file_size = (int) LP_Settings::get_option( 'material_max_file_size', 2 ); + $allow_upload_amount = (int) LP_Settings::get_option( 'material_upload_files', 2 ); + // check file was uploaded + $count_uploaded_files = count( $material_db->get_material_by_item_id( $item_id, 0, 0, 1 ) ); + // check file amount which can upload + $can_upload = $allow_upload_amount - $count_uploaded_files; + + //Check file amount validation + if ( $can_upload <= 0 ) { + throw new Exception( esc_html__( 'Material feature is not allowed to upload', 'learnpress' ) ); + } elseif ( $allow_upload_amount > 0 ) { + if ( count( $material_data ) > $can_upload ) { + throw new Exception( esc_html__( 'Your uploaded files reach the maximum amount!', 'learnpress' ) ); + } + } + + $file_methods = array( 'upload', 'external' ); + $error_messages = ''; + $success_messages = ''; + foreach ( $material_data as $key => $material ) { + $label = LP_Helper::sanitize_params_submitted( $material['label'] ?? '' ); + $method = $material['method'] ?? ''; + $file_name = $file['name'][ $key ] ?? ''; + + // check file title + if ( empty( $label ) ) { + $error_messages .= sprintf( __( 'File "%s" title is not empty!', 'learnpress' ), $file_name ); + continue; + } + // check file upload method + if ( ! in_array( $method, $file_methods ) ) { + $error_messages .= sprintf( __( 'File %s method is invalid!', 'learnpress' ), $label ); + continue; + } + + $file_path = ''; + if ( $method == 'upload' ) { + if ( $file['size'][ $key ] > $max_file_size * 1024 * 1024 ) { + $error_messages .= sprintf( __( 'File %s size is too large!', 'learnpress' ), $label ); + continue; + } + + // Check type file + $file_info = wp_check_filetype( $file_name ); + $file_type = $file_info['ext'] ?? ''; + if ( empty( $file_info['ext'] ) + || false === $this->material_check_file_extention( $file_info['ext'] ) + || ! in_array( $file_info['type'], get_allowed_mime_types() ) ) { + $error_messages .= sprintf( esc_html__( 'File %s type is invalid!', 'learnpress' ), $label ); + continue; + } + + LP_WP_Filesystem::instance(); + $file_uploading = [ + 'name' => $file_name, + 'type' => $file_type, + 'tmp_name' => $file['tmp_name'][ $key ], + 'error' => $file['error'][ $key ], + 'size' => $file['size'][ $key ], + ]; + $file_handle_upload = wp_handle_upload( $file_uploading, [ 'test_form' => false ] ); + if ( ! empty( $file_handle_upload['error'] ) ) { + $error_messages .= sprintf( esc_html__( 'File %s: ', 'learnpress' ), $label ) . $file_handle_upload['error']; + continue; + } + + $file_path = str_replace( wp_upload_dir()['baseurl'], '', $file_handle_upload['url'] ); + } elseif ( $method == 'external' ) { + $file_path = sanitize_url( $material['link'] ); + $file_info = pathinfo( $file_path ); + $file_extend = $file_info['extension'] ?? ''; + if ( ! $file_extend ) { + $file_type = __( 'Unknown', 'learnpress' ); + } else { + $file_type = $file_extend; + } + $check_allow_file = $this->check_wp_allowed_file_type( $file_extend ); + if ( ! $check_allow_file ) { + $error_messages .= sprintf( + esc_html__( 'An error occurred while checking %1$s. %2$s', 'learnpress' ), + $label, + __( 'Oops! That file type isn’t allowed.', 'learnpress' ) + ); + continue; + } + + // For a long time remove this code. @since 4.2.6.6 + $material_db->wpdb->query( + "ALTER TABLE $material_db->tb_lp_files MODIFY file_type VARCHAR(100) NOT NULL DEFAULT '';" + ); + } + + $orders = $count_uploaded_files + $key + 1; + $insert_arr = [ + 'file_name' => sanitize_text_field( $label ), + 'file_type' => $file_type ?? '', + 'item_id' => (int) $item_id, + 'item_type' => get_post_type( $item_id ), + 'method' => $method, + 'file_path' => $file_path, + 'orders' => $orders, + 'created_at' => current_time( 'Y-m-d H:i:s' ), + ]; + $insert = $material_db->create_material( $insert_arr ); + if ( ! $insert ) { + $error_messages .= sprintf( __( 'Cannot save file %s', 'learnpress' ), $label ); + continue; + } + + $success_messages .= __( 'Other files is upload successfully.', 'learnpress' ); + $response->data[] = [ + 'file_name' => $label, + 'method' => ucfirst( $method ), + 'file_id' => $insert, + 'orders' => $orders, + ]; + } + + if ( ! empty( $error_messages ) ) { + $response->message .= $error_messages; + } + + if ( ! empty( $success_messages ) ) { + $response->status = 'success'; + if ( empty( $error_messages ) ) { + $success_messages = __( 'Files upload successfully.', 'learnpress' ); + } + $response->message .= $success_messages; + } + } catch ( Throwable $e ) { + $response->message = $e->getMessage(); + } + + return $response; + } + + /** + * Get list files of a course or a lesson (use in wp-admin) + * + * @param WP_REST_Request $request + * + * @return LP_REST_Response + * @version 1.0.2 + * @since 4.2.2 + */ + public function backend_get_materials_by_item( WP_REST_Request $request ): LP_REST_Response { + $response = new LP_REST_Response(); + + try { + $params = $request->get_params(); + $item_id = $params['item_id'] ?? 0; + if ( ! $item_id ) { + throw new Exception( esc_html__( 'Invalid item id!', 'learnpress' ) ); + } + + $material_db = LP_Material_Files_DB::getInstance(); + $page = absint( $params['page'] ?? 1 ); + $per_page = $params['per_page'] ?? (int) LP_Settings::get_option( 'material_file_per_page', - 1 ); + $offset = ( $per_page > 0 && $page > 1 ) ? $per_page * ( $page - 1 ) : 0; + $item_materials = $material_db->get_material_by_item_id( $item_id, $per_page, $offset, true ); + + if ( $item_materials ) { + $response->data->items = $item_materials; + $response->message = esc_html__( 'Successfully', 'learnpress' ); + } else { + $response->message = esc_html__( 'Empty material!', 'learnpress' ); + } + + $response->status = 'success'; + } catch ( Throwable $th ) { + $response->message = $th->getMessage(); + } + + return $response; + } + + /** + * Get info file from external link + * + * @param $file_url + * + * @return array + * @version 1.0.1 + * @since 4.2.2 + * @depreacted 4.2.7.8.5 + */ + public function check_external_file( $file_url ): array { + $lp_file = LP_WP_Filesystem::instance(); + $temp_file = $lp_file->download_url( $file_url ); + $file = []; + if ( is_wp_error( $temp_file ) ) { + $file['error'] = true; + $file['error_message'] = $temp_file->get_error_message(); + } else { + $file = array( + 'name' => basename( $file_url ), + 'type' => mime_content_type( $temp_file ), + 'tmp_name' => $temp_file, + 'size' => filesize( $temp_file ), + 'error' => false, + 'error_message' => '', + ); + } + + return $file; + } + + /** + * @param [string] $file_name [upload file name] + * @param [] $file_tmp [file content] + * + * @return [array] [file infomations] + * @since 4.2.2 + * [material_upload_file upload file when user choose upload method] + * @version 1.0.0 + */ + public function material_upload_file( $file_name, $file_tmp ) { + try { + $file = wp_upload_bits( $file_name, null, file_get_contents( $file_tmp ) ); + + return $file['error'] ? false : $file; + } catch ( Throwable $e ) { + error_log( $e->getMessage() ); + } + } + + /** + * @param [string] $ext [file extendsion] + * + * @return [string] [file extendsion] + * @version 1.0.0 + * @since 4.2.2 + * [material_check_file_extention return file type of file] + */ + public function material_check_file_extention( $ext ) { + $allow_file_type = LP_Settings::get_option( 'material_allow_file_type', array( 'pdf', 'txt' ) ); + $allow_file_type = implode( ',', $allow_file_type ); + $allow_file_type = explode( ',', $allow_file_type ); + + return in_array( $ext, $allow_file_type ) ? $ext : false; + } + + + public function update_material_orders( WP_REST_Request $request ) { + $response = new LP_REST_Response(); + try { + $item_id = $request['item_id']; + $sort_arr = $request->get_param( 'sort_arr' ); + $sort_arr = json_decode( wp_unslash( $sort_arr ), true ); + $material_db = LP_Material_Files_DB::getInstance(); + $update_sort = $material_db->update_material_orders( $sort_arr, $item_id ); + if ( $update_sort ) { + $response->status = 200; + $response->message = esc_html__( 'Updated.', 'learnpress' ); + // $response->data = $sort_arr; + } else { + throw new Exception( esc_html__( 'Update failed!', 'learnpress' ) ); + } + } catch ( Throwable $e ) { + $response->status = 400; + $response->message = $e->getMessage(); + } + + return rest_ensure_response( $response ); + } + + /** + * @param [type] $request [description] + * + * @return [json] [return message] + * @version 1.0.0 + * @since 4.2.2 + * [delete_material delete a material when a delete request is send] + */ + public function delete_material( WP_REST_Request $request ) { + $response = new LP_REST_Response(); + try { + $file_id = $request['file_id']; + if ( ! $file_id ) { + throw new Exception( esc_html__( 'Invalid file identifier', 'learnpress' ) ); + } + // DB Init + $material_db = LP_Material_Files_DB::getInstance(); + $file = $material_db->get_material( $file_id ); + if ( ! $file ) { + throw new Exception( esc_html__( 'File not found', 'learnpress' ) ); + } + + // Get file author + $item_id = $file->item_id ?? 0; + $author = get_post_field( 'post_author', $item_id ); + $current_user_id = get_current_user_id(); + // check permission + if ( ! ( ( $author == $current_user_id && current_user_can( LP_TEACHER_ROLE ) ) || current_user_can( ADMIN_ROLE ) ) ) { + throw new Exception( esc_html__( 'You do not have permission to delete this file.', 'learnpress' ) ); + } + + // Delete record + $delete = $material_db->delete_material( $file_id ); + if ( $delete ) { + $message = esc_html__( 'File is deleted.', 'learnpress' ); + $deleted = true; + } else { + $message = esc_html__( 'There is an error when delete this file.', 'learnpress' ); + $deleted = false; + } + $response->status = 200; + $response->delete = $deleted; + $response->message = $message; + } catch ( Throwable $th ) { + $response->status = 400; + $response->message = $th->getMessage(); + } + + return rest_ensure_response( $response ); + } +
AI Analysis
I'll analyze this code diff for security vulnerabilities following the specified format.
---
**Vulnerability Existed: yes**
TRUE POSITIVE
**Broken Access Control (CWE-639) - CWE-639 - class-lp-rest-material-controller.php lines 430-450**
**Old Code:**
```php
public function delete_material( WP_REST_Request $request ) {
$response = new LP_REST_Response();
try {
$id = $request['file_id'];
if ( ! $id ) {
throw new Exception( esc_html__( 'Invalid file identifier', 'learnpress' ) );
}
// DB Init
$material_db = LP_Material_Files_DB::getInstance();
// Delete record
$delete = $material_db->delete_material( $id );
if ( $delete ) {
$message = esc_html__( 'File is deleted.', 'learnpress' );
$deleted = true;
} else {
$message = esc_html__( 'There is an error when delete this file.', 'learnpress' );
$deleted = false;
}
$response->status = 200;
$response->delete = $deleted;
$response->message = $message;
} catch ( Throwable $th ) {
$response->status = 400;
$response->message = $th->getMessage();
}
return rest_ensure_response( $response );
}
```
**Fixed Code:**
```php
public function delete_material( WP_REST_Request $request ) {
$response = new LP_REST_Response();
try {
$file_id = $request['file_id'];
if ( ! $file_id ) {
throw new Exception( esc_html__( 'Invalid file identifier', 'learnpress' ) );
}
// DB Init
$material_db = LP_Material_Files_DB::getInstance();
$file = $material_db->get_material( $file_id );
if ( ! $file ) {
throw new Exception( esc_html__( 'File not found', 'learnpress' ) );
}
// Get file author
$item_id = $file->item_id ?? 0;
$author = get_post_field( 'post_author', $item_id );
$current_user_id = get_current_user_id();
// check permission
if ( ! ( ( $author == $current_user_id && current_user_can( LP_TEACHER_ROLE ) ) || current_user_can( ADMIN_ROLE ) ) ) {
throw new Exception( esc_html__( 'You do not have permission to delete this file.', 'learnpress' ) );
}
// Delete record
$delete = $material_db->delete_material( $file_id );
if ( $delete ) {
$message = esc_html__( 'File is deleted.', 'learnpress' );
$deleted = true;
} else {
$message = esc_html__( 'There is an error when delete this file.', 'learnpress' );
$deleted = false;
}
$response->status = 200;
$response->delete = $deleted;
$response->message = $message;
} catch ( Throwable $th ) {
$response->status = 400;
$response->message = $th->getMessage();
}
return rest_ensure_response( $response );
}
```
**Explanation:**
The vulnerability was an **authorization bypass** in the `delete_material()` method. While the route has a `permission_callback` that checks `check_user_can_edit_material()` at the controller level, this callback only verifies the user's relationship to the **item** (course/lesson).
The vulnerability occurred because:
1. **Old code** deleted any material file by ID without verifying the current user owns/has permission to delete that specific file resource
2. An authenticated teacher could discover material file IDs from other courses/lessons and delete them directly via the REST API
3. The permission check only validated access to the item, not the specific material file being deleted
**The fix** implements per-resource authorization by:
1. Fetching the material file first to get its metadata
2. Extracting the item_id from the file
3. Checking if the current user is the author of that item AND has the teacher role, OR is an admin
4. Only proceeding with deletion if authorization passes
This is a critical security fix that prevents unauthorized deletion of material files across the system.
CVE Analysis Results:
CVE-2025-14802: Yes
View CVE Description
The LearnPress – WordPress LMS Plugin for WordPress is vulnerable to unauthorized file deletion in versions up to, and including, 4.3.2.2 via the /wp-json/lp/v1/material/{file_id} REST API endpoint. This is due to a parameter mismatch between the DELETE operation and authorization check, where the endpoint uses file_id from the URL path but the permission callback validates item_id from the request body. This makes it possible for authenticated attackers, with teacher-level access, to delete arbitrary lesson material files uploaded by other teachers via sending a DELETE request with their own item_id (to pass authorization) while targeting another teacher's file_id.
Showing 1 to 6 of 6 results