SQL Injection Archives - Patchstack Easily secure your websites from plugin vulnerabilities! Thu, 12 Feb 2026 13:00:24 +0000 en-US hourly 1 https://patchstack.com/wp-content/uploads/2026/01/cropped-favicon-32x32.png SQL Injection Archives - Patchstack 32 32 SQL Injection Vulnerability in Quiz and Survey Master (QSM) Plugin Affecting 40k+ Sites https://patchstack.com/articles/sql-injection-vulnerability-in-quiz-and-survey-master-qsm-plugin-affecting-40k-sites/ https://patchstack.com/articles/sql-injection-vulnerability-in-quiz-and-survey-master-qsm-plugin-affecting-40k-sites/#respond Thu, 29 Jan 2026 11:59:07 +0000 https://patchstack.com/?p=25435 This blog post is about a Subscriber+ SQL injection vulnerability in the Quiz and Survey Master (QSM) plugin. If you're a QSM user, please update to at least version 10.3.2. This vulnerability was discovered and reported by Patchstack Alliance community member Doan Dinh Van. About the Quiz and Survey Master plugin The QSM plugin, with […]

The post SQL Injection Vulnerability in Quiz and Survey Master (QSM) Plugin Affecting 40k+ Sites appeared first on Patchstack.

]]>

Quiz and Survey Master (QSM)

SQL Injection

40K
CVSS 8.5

This blog post is about a Subscriber+ SQL injection vulnerability in the Quiz and Survey Master (QSM) plugin. If you're a QSM user, please update to at least version 10.3.2.

This vulnerability was discovered and reported by Patchstack Alliance community member Doan Dinh Van.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the Quiz and Survey Master plugin

The QSM plugin, with over 40,000 active installations, is a plugin for creating quizzes, surveys, and forms. It includes advanced features like multimedia support and a drag-and-drop quiz builder.

A screenshot of the plugin's animated banner, promoting the plugin's features.

The security vulnerability

In versions 10.3.1 and below, the QSM plugin is vulnerable to SQL injection, allowing any logged-in user to inject commands into the database. This means any Subscriber or higher user is able to perform a wide variety of unwanted actions, including potentially extracting sensitive information stored in the site's database.

This vulnerability has been patched in version 10.3.2 and is tracked with CVE-2025-67987.

The root cause of the issue lies in the qsm_rest_get_question function:

function qsm_rest_get_question( WP_REST_Request $request ) {
	// Makes sure user is logged in.
	if ( is_user_logged_in() ) {
		global $wpdb;
		$current_user = wp_get_current_user();
		if ( 0 !== $current_user ) {
			$question       = QSM_Questions::load_question( $request['id'] );
			$categorysArray = QSM_Questions::get_question_categories( $question['question_id'] );
			if ( ! empty( $question ) ) {
				$is_linking = $request['is_linking'];
				$comma_separated_ids = '';
				if ( 1 <= $is_linking ) {
					if ( isset( $question['linked_question'] ) && '' == $question['linked_question'] ) {
						$comma_separated_ids = $is_linking;
					} else {
						$linked_question = isset($question['linked_question']) ? $question['linked_question'] : '';
						$exploded_question_array = explode(',', $linked_question);
						if ( ! empty($linked_question) ) {
							$exploded_question_array = array_merge([ $is_linking ], $exploded_question_array);
						} else {
							$exploded_question_array = [ $is_linking ];
						}
						$comma_separated_ids = implode(',', array_unique($exploded_question_array));
					}
				}

				$quiz_name_by_question = array();
				if ( ! empty($comma_separated_ids) ) {
					$quiz_results = $wpdb->get_results( "SELECT `quiz_id`, `question_id` FROM `{$wpdb->prefix}mlw_questions` WHERE `question_id` IN (" .$comma_separated_ids. ")" );
--------------------------- CUT HERE --------------------------- 

The plugin works off an assumption that the is_linking parameter is an ID, however it does no validation or sanitizing of the parameter's value before including it in a larger list of question IDs. This list is eventually included directly in an SQL statement (WHERE `question_id` IN (" .$comma_separated_ids. ")"). Because the value is not validated (e.g., with is_int/intval to ensure the value is a number), and the SQL statement is not using a prepared statement (which ensures the value is sanitized before being integrated into the SQL query), a malicious user could send an abnormal value containing an SQL statement, and have that statement be executed as part of the $quiz_results query.

The patch

In version 10.3.2, the vulnerability is mitigated by validating the content of the is_linking parameter with intval. This forces the value to be an integer, regardless of the original content. By ensuring the value is an integer, it can be safely added to a query without risk of injection, as no additional SQL commands could be included.

A screenshot of a diff between 10.3.1 and 10.3.2, showing the fix.

Conclusion

Database calls can be dangerous. User-provided input can be untrustworthy. Combining the two is a recipe for disaster. Even when a particular value isn't intended to be directly provided by a user, any input coming from a request can be modified and needs to be validated before use.

Any input from the user should be validated, sanitized, or both before use. In this case, intval was used to both ensure the value was a number and sanitize it by forcing the value to become a number. When the data is able to be validated as something known and safe, that's great. For anything that may be free form or more complicated to validate, sanitization is a must. PHP itself includes some functions for type validation, and WordPress offers many other sanitization functions for many common use cases.

Regarding SQL specifically, prepared statements are highly recommended. Prepared statements are used to tell the database or the database access APIs, "This is the actual query" and "this is data for the query"; this means the data can be treated specifically as data, and its content won't be used as part of the query that contains it. Whenever possible, we recommend using wpdb::prepare on any SQL queries that may contain untrusted data.

Timeline

21 November 2025We received the vulnerability report and notified the vendor.
04 December 2025The vendor released 10.3.2, which patched this vulnerability.
28 January 2026We published the vulnerability entry to the Patchstack database.
29 January 2026Security advisory article publicly released.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base!

The post SQL Injection Vulnerability in Quiz and Survey Master (QSM) Plugin Affecting 40k+ Sites appeared first on Patchstack.

]]>
https://patchstack.com/articles/sql-injection-vulnerability-in-quiz-and-survey-master-qsm-plugin-affecting-40k-sites/feed/ 0
SQL Injection Vulnerability Patched in Paid Membership Subscriptions Plugin https://patchstack.com/articles/sql-injection-vulnerability-patched-in-paid-membership-subscriptions-plugin/ https://patchstack.com/articles/sql-injection-vulnerability-patched-in-paid-membership-subscriptions-plugin/#respond Thu, 28 Aug 2025 08:40:16 +0000 https://patchstack.com/?p=24056 The WP Job Portal plugin, with over 8,000 active installs, is vulnerable to unauthenticated SQL injection and arbitrary file read vulnerability.

The post SQL Injection Vulnerability Patched in Paid Membership Subscriptions Plugin appeared first on Patchstack.

]]>

This blog post is about an unauthenticated SQL injection vulnerability in the Paid Membership Subscriptions plugin. If you're a Paid Membership Subscriptions plugin user, please update the plugin to version 2.15.2.

The vulnerabilities mentioned here were discovered and reported by Patchstack Alliance community member ChuongVN.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About Paid Membership Subscriptions plugin

The plugin Paid Membership Subscriptions, which has over 10,000 active installations, allows site owners to optimize the site with membership and recurring subscriptions in just a few clicks. It allows integration of various payment methods and offers a smooth subscription styled payments.

Unauthenticated SQL Injection

In versions 2.15.1 and below, the plugin is vulnerable to an SQL injection, which allows any unauthenticated attacker to inject SQL queries into the database. The vulnerability has been patched in version 2.15.2 and is tracked with CVE-2025-49870.

The root cause of the issue lies in the process_webhooks function:

public function process_webhooks() {

    if( !isset( $_GET['pay_gate_listener'] ) || $_GET['pay_gate_listener'] !== 'paypal_ipn' )
        return;

    // Init IPN Verifier
    $ipn_verifier = new PMS_IPN_Verifier();

    if( pms_is_payment_test_mode() )
        $ipn_verifier->is_sandbox = true;


    $verified = false;

    // Process the IPN
    try {
        if( $ipn_verifier->checkRequestPost() )
            $verified = $ipn_verifier->validate();
    } catch ( Exception $e ) {

    }


    if( $verified ) {

        $post_data = $_POST;

        // Get payment id from custom variable sent by IPN
        $payment_id = isset( $post_data['custom'] ) ? $post_data['custom'] : 0;

        // Get the payment
        $payment = pms_get_payment( $payment_id );

        // Get user id from the payment
        $user_id = $payment->user_id;

        // TRIMMED
    }
}

The function takes the user-input as $post_data, extracts the $payment_id value from it, and calls the pms_get_payment function.

function pms_get_payment( $payment_id = 0 ) {

    return new PMS_Payment( $payment_id );

}

The function just calls a new instance of the PMS_Payment class. The constructor of the respective class gets called.

public function __construct( $id = 0 ) {

    // Return if no id provided
    if( $id == 0 ) {
        $this->id = 0;
        return;
    }

    // Get payment data from the db
    $data = $this->get_data( $id );

    // Return if data is not in the db
    if( is_null($data) ) {
        $this->id = 0;
        return;
    }

    // Populate the data
    $this->set_instance( $data );

}

The constructor of the class calls get_data which is vulnerable to SQL injection due to improper concatenation of user input.

public function get_data( $id ) {

    global $wpdb;

    $result = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}pms_payments WHERE id = {$id}", ARRAY_A );

    return $result;

}

The patch

In version 2.15.2, the vendor patched the SQL injection by ensuring that $id is numeric while passing to the get_data function and used proper prepared statements.

Conclusion

For the SQL query process, always do a safe escape and format the user's input before performing a query. The best practice is always to use a prepared statement and also cast each of the used variables to its intended usage.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

02 June, 2025We received the vulnerability report and notified the vendor.
11 June, 2025The vendor publishes the patched version to the WP repository.
03 July, 2025We published the vulnerability entry to the database.
28 August, 2025Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post SQL Injection Vulnerability Patched in Paid Membership Subscriptions Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/sql-injection-vulnerability-patched-in-paid-membership-subscriptions-plugin/feed/ 0
SQL Injection Vulnerability Found in LifterLMS Plugin Affecting 10K+ Sites https://patchstack.com/articles/sql-injection-vulnerability-found-in-lifterlms-plugin-affecting-10k-sites/ https://patchstack.com/articles/sql-injection-vulnerability-found-in-lifterlms-plugin-affecting-10k-sites/#respond Wed, 20 Aug 2025 12:21:09 +0000 https://patchstack.com/?p=23915 This blog post is about LifterLMS theme vulnerabilities. If you're a LifterLMS user, please update the plugin to version 8.0.7 or higher. About the LifterLMS Theme and Plugin The LifterLMS plugin, which has over 10,000 installations, is a secure, easy-to-use WordPress LMS plugin packed with features to easily create & sell courses online. The security […]

The post SQL Injection Vulnerability Found in LifterLMS Plugin Affecting 10K+ Sites appeared first on Patchstack.

]]>

LifterLMS

Unauthenticated SQL Injection

10k
CVSS 9.3

This blog post is about LifterLMS theme vulnerabilities. If you're a LifterLMS user, please update the plugin to version 8.0.7 or higher.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the LifterLMS Theme and Plugin

The LifterLMS plugin, which has over 10,000 installations, is a secure, easy-to-use WordPress LMS plugin packed with features to easily create & sell courses online.

The security vulnerability

This plugin suffers from an SQL Injection vulnerability. The SQL Injection vulnerability itself allows any unauthenticated and also authenticated user to inject a malicious SQL query into a WordPress database query execution. The described vulnerability is patched in version 8.0.7 and assigned CVE-2025-52717.

The underlying vulnerable code exists in the get_voucher_by_code function:

        public function get_voucher_by_code( $code ) {

            global $wpdb;

            $table          = $this->get_codes_table_name();
            $redeemed_table = $this->get_redemptions_table_name();

            $query = "SELECT c.*, count(r.id) as used
                    FROM $table as c
                    LEFT JOIN $redeemed_table as r
                    ON c.`id` = r.`code_id`
                    WHERE `code` = '$code' AND `is_deleted` = 0
                    GROUP BY c.id
                    LIMIT 1";
            return $wpdb->get_row( $query );
        }

To be able to exploit the vulnerability, registration should be enabled on the website. The vulnerability only exists on the installations that allow students to register with a voucher code. The user input is sanitize_text_field(), but it is not enough for SQL Injection attacks. A similar function with the name redeem_voucher() also exists in a different part of the code.

    public function redeem_voucher() {

		if ( ! llms_verify_nonce( 'lifterlms_voucher_nonce', 'lifterlms_voucher_check' ) || ! get_current_user_id() ) {
			return null;
		}

		$voucher  = new LLMS_Voucher();
		$redeemed = $voucher->use_voucher( llms_filter_input_sanitize_string( INPUT_POST, 'llms_voucher_code' ), get_current_user_id() );

		if ( is_wp_error( $redeemed ) ) {
			llms_add_notice( $redeemed->get_error_message(), 'error' );
			return $redeemed;
		}

		llms_add_notice( __( 'Voucher redeemed successfully!', 'lifterlms' ), 'success' );
		return true;
	}

However, in this part of the code user inputted voucher value goes through the llms_filter_input_sanitize_string() function, which has proper sanitation.

function llms_filter_input_sanitize_string( $type, $variable_name, $flags = array() ) {

	$require_array = in_array( FILTER_REQUIRE_ARRAY, $flags, true );

	$string = llms_filter_input( $type, $variable_name, FILTER_UNSAFE_RAW, $require_array ? FILTER_REQUIRE_ARRAY : array() );

	// If we have an empty string or the input var isn't found we can return early.
	if ( empty( $string ) ) {
		return $string;
	}

	$string = $require_array ? array_map( 'wp_strip_all_tags', $string ) : wp_strip_all_tags( $string );

	if ( ! in_array( FILTER_FLAG_NO_ENCODE_QUOTES, $flags, true ) ) {
		$string = str_replace(
			array( "'", '"' ),
			array( '&#39;', '&#34;' ),
			$string
		);
	}

	return $string;
}

The patch

The developer decided to use a proper prepared statement to the affected variables to prevent SQL Injection. The patch can be seen in the below diff image:

Conclusion

For the SQL query process, always do a safe escape and format the user's input before performing a query. The best practice is always to use a prepared statement and also cast each of the used variables to its intended usage, for example, always cast a variable to an integer if the intended value of the variable should be an integer value.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

3 June, 2025Vulnerability validated by us.
3 June, 2025We reach out to the vendor regarding the vulnerability.
11 June, 2025Vendor released and sent Patchstack the proposed fix.
01 July, 2025Vulnerability released on the Patchstack database
20 August, 2025Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post SQL Injection Vulnerability Found in LifterLMS Plugin Affecting 10K+ Sites appeared first on Patchstack.

]]>
https://patchstack.com/articles/sql-injection-vulnerability-found-in-lifterlms-plugin-affecting-10k-sites/feed/ 0
Multiple Critical Vulnerabilities Patched in WP Job Portal Plugin https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wp-job-portal-plugin/ https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wp-job-portal-plugin/#respond Tue, 12 Aug 2025 11:06:15 +0000 https://patchstack.com/?p=23492 The WP Job Portal plugin, with over 8,000 active installs, is vulnerable to unauthenticated SQL injection and arbitrary file read vulnerability.

The post Multiple Critical Vulnerabilities Patched in WP Job Portal Plugin appeared first on Patchstack.

]]>

WP Job Portal Plugin

SQL Injection

8k
CVSS 9.3

This blog post is about an unauthenticated arbitrary file download and an SQL injection vulnerability in the WP Job Portal plugin. If you're a WP Job Portal plugin user, please update the plugin to version 2.3.3.

The vulnerabilities mentioned here were discovered and reported by Patchstack Alliance community member LVT-tholv2k.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About WP Job Portal plugin

The plugin WP Job Portal, which has over 8,000 active installations, allows site owners to deliver a complete recruitment system without having to code. It helps make the hiring process easier, faster, and more accurate.

Unauthenticated SQL Injection

In versions 2.3.2 and below, the plugin is vulnerable to an SQL injection, which allows any unauthenticated attacker to inject SQL queries into the database. The vulnerability has been patched in version 2.3.3 and is tracked with CVE-2025-48274.

The root cause of the issue lies in the validateFormData function:

function validateFormData(&$data) {
    $category = WPJOBPORTALrequest::getVar('parentid');
    $inquery = ' ';
    if ($category) {
        $inquery .=" WHERE parentid = $category ";
    }
    $canupdate = false;
    if ($data['id'] == '') {
        $result = $this->isCategoryExist($data['cat_title']);
        if ($result == true) {
            return WPJOBPORTAL_ALREADY_EXIST;
        } else {
            $query = "SELECT max(ordering)+1 AS maxordering FROM " . wpjobportal::$_db->prefix . "wj_portal_categories " . $inquery;
            $data['ordering'] = wpjobportaldb::get_var($query);
            if ($data['ordering'] == null)
                $data['ordering'] = 1;
        }
//TRIMMED
    }
    return $canupdate;
}

The function validateFormData is taking the $category value from the user-input and concatenating it with the SQL statement variable $inquery. The variable is then concatenated to $query and executed as an SQL statement, which leads to SQLi.

The function validateFormData is called by another function storeCategory , which again is called by the function savecategory which can be executed as a task by a user.

Unauthenticated Arbitrary File Download

In versions 2.3.2 and below, the plugin is vulnerable to an arbitrary file upload, which allows any unauthenticated attacker to download any file from the server. The vulnerability has been patched in version 2.3.3 and is tracked with CVE-2025-48273.

The root cause of the issue lies in the downloadCustomUploadedFile function:

function downloadCustomUploadedFile($upload_for,$file_name,$entity_id){

    $filename = wpjobportalphplib::wpJP_str_replace(' ', '_', $file_name);
    $maindir = wp_upload_dir();
    $basedir = $maindir['basedir'];
    $datadirectory = wpjobportal::$_config->getConfigurationByConfigName('data_directory');

    $path = $basedir . '/' . $datadirectory. '/data';

//TRIMMED

    $file = $path .'/'.$file_name;
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename=' . wpjobportalphplib::wpJP_basename($file));
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header('Pragma: public');
    header('Content-Length: ' . filesize($file));
    //ob_clean();
    flush();
    if ( ! function_exists( 'WP_Filesystem' ) ) {
        require_once ABSPATH . 'wp-admin/includes/file.php';
    }
    global $wp_filesystem;
    if ( ! is_a( $wp_filesystem, 'WP_Filesystem_Base') ) {
        $creds = request_filesystem_credentials( site_url() );
        wp_filesystem( $creds );
    }
    // test
    echo $wp_filesystem->get_contents($file);
    exit();
}

The function is taking the parameters and directly concatenating the $file_name variable to $file and retrieving and outputting the file. Tracing back the downloadCustomUploadedFile function, we come across the downloadcustomfile function.

function downloadcustomfile(){
    $nonce = WPJOBPORTALrequest::getVar('_wpnonce');
    if (! wp_verify_nonce( $nonce, 'wpjobportal_field_nonce') ) {
            die( 'Security check Failed' );
    }

    $upload_for = WPJOBPORTALrequest::getVar('upload_for');// to handle different entities(company, job, resume)
    $entity_id = WPJOBPORTALrequest::getVar('entity_id');// to create path for enitity directory where the file is located
    $file_name = WPJOBPORTALrequest::getVar('file_name');// to access the file and download it

    $result = WPJOBPORTALincluder::getJSModel('customfield')->downloadCustomUploadedFile($upload_for,$file_name,$entity_id);
    $msg = WPJOBPORTALMessages::getMessage($result, 'customfield');
    WPJOBPORTALMessages::setLayoutMessage($msg['message'], $msg['status'],$this->_msgkey);
    $url = esc_url_raw(admin_url("admin.php?page=wpjobportal_customfield&ff=".esc_attr($ff)));
    if ($pagenum)
        $url .= "&pagenum=" . $pagenum;
    wp_redirect($url);
    die();
}

The function downloadcustomfile, which is also another task that can be executed by a user, takes $file_name from the user and passes it to downloadCustomUploadedFile. As mentioned above, the value is directly concatenated and retrieved, meaning that it is possible to access any file with path traversal.

Note: Both actions require a nonce to be executed , which is accessible through a page with the shortcode [wpjobportal_my_resumes].

The patch

In version 2.3.3, the vendor patched the SQL injection by ensuring that $category is numeric and added additional esc_sql() check on the variable.

For arbitrary file upload vulnerability, they patched the issue by adding a filename check with wpJP_clean_file_path function.

Removing all occurrences of ./ and .. essentially prevents an attacker from doing path traversal to access an arbitrary file.

Conclusion

It is necessary to ensure that all the SQL queries are properly used with prepared statements when processing with user-inputs. Whenever it comes to downloading or retrieving files, there should be proper filename validation to ensure there is no path traversal leading to arbitrary file access.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

28 April, 2025We received the vulnerability report and notified the vendor.
13 May, 2025The vendor publishes the patched version to the WP repository.
24 May, 2025We published the vulnerability entries to the DB- SQLi & Arbitrary File Download.
12 August, 2025Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Multiple Critical Vulnerabilities Patched in WP Job Portal Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wp-job-portal-plugin/feed/ 0
Critical Vulnerabilities Found in Fancy Product Designer Plugin https://patchstack.com/articles/critical-vulnerabilities-found-in-fancy-product-designer-plugin/ https://patchstack.com/articles/critical-vulnerabilities-found-in-fancy-product-designer-plugin/#respond Wed, 08 Jan 2025 13:21:58 +0000 https://patchstack.com/?p=20217 This blog post is about Fancy Product Designer plugin vulnerabilities. If you're a Fancy Product Designer user, please delete or deactivate the plugin until the patch is released by the vendor. About the Fancy Product Designer Plugin The plugin Fancy Product Designer (premium version), which has over 20,000 sales, is one of the more popular […]

The post Critical Vulnerabilities Found in Fancy Product Designer Plugin appeared first on Patchstack.

]]>

Fancy Product Designer

Unauthenticated Arbitrary File Upload

20k
CVSS 9.0

Fancy Product Designer

Unauthenticated SQL Injection

20k
CVSS 9.3

This blog post is about Fancy Product Designer plugin vulnerabilities. If you're a Fancy Product Designer user, please delete or deactivate the plugin until the patch is released by the vendor.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the Fancy Product Designer Plugin

The plugin Fancy Product Designer (premium version), which has over 20,000 sales, is one of the more popular premium plugins specifically related to customizing and designing all kinds of products from WooCommerce. This plugin is developed by Radykal.

This plugin will enable users to design and customize any product. Limited only by the user's imagination it gives users absolute freedom in deciding which products and which parts of the product can be customized.

The security vulnerability

This plugin suffers from Unauthenticated Arbitrary File Upload, where users can upload arbitrary files including PHP files to the server, resulting in a Remote Code Execution (RCE).

The second vulnerability is Unauthenticated SQL Injection which allows any users to execute arbitrary SQL queries in the database of the WordPress site.

The described vulnerabilities are still unpatched in the latest version known (6.4.3) and assigned CVE-2024-51919 and CVE-2024-51818 respectively.

Unauthenticated Arbitrary File Upload

The underlying vulnerable code exists in the save_remote_file and fpd_admin_copy_file functions:

public static function save_remote_file( $remote_file_url ) {

    $unique_dir = time().bin2hex(random_bytes(16));
    $temp_dir = FPD_ORDER_DIR . 'print_ready_files/' . $unique_dir;
    mkdir($temp_dir);

    $local_file_path = $temp_dir;

    $filename = fpd_admin_copy_file(
        $remote_file_url,
        $local_file_path
    );

    return $filename ? $unique_dir . '/' . $filename : null;

}
function fpd_admin_copy_file( $file_url, $destination_dir ) {

	if( empty( $file_url ) ) return false;

	if( !file_exists($destination_dir) )
        wp_mkdir_p( $destination_dir );

	$filename = basename( $file_url );

	if( function_exists('copy') ) {

		return copy( $file_url, $destination_dir . '/' . $filename ) ? $filename : false;

	}
	else {

		$content = file_get_contents( $file_url );
		$fp = fopen( $destination_dir . '/' . $filename, 'w' );
		$bytes = fwrite( $fp, $content );
		fclose( $fp );

		return $bytes !== false ? $filename : false;

	}

}

The save_remote_file function itself will just accept a remote URL value via $remote_file_url and it will call the fpd_admin_copy_file function to copy or save the files. If we look closely at the fpd_admin_copy_file function, it will either save the file with the filename set to the basename of $file_url using copy or write function. Since there is no proper check on those two functions, if there are any codes that utilize those functions without additional file checks, then we can achieve arbitrary file upload.

We found that the save_remote_file function can be called from the webhook_create_pr_file function:

public function webhook_create_pr_file( $request ) {

    $response = array();
    
    if( $request->get_param('print_job_id') && $request->get_param('file_url') ) {
            
        $print_job = new FPD_Print_Job( $request->get_param('print_job_id'), true );
        $print_job_details = $print_job->get_details();
            
        //check if print job exists
        if( $print_job_details ) {

            $remote_file_url = $request->get_param('file_url');
            $response['file_url'] = $remote_file_url;

            $local_file = FPD_Pro_Export::save_remote_file( $remote_file_url );
------------------ CUT HERE ------------------

The function itself is a handler to a custom REST API endpoint registered on:

public function register_routes() {

    register_rest_route( FPD_Pro_Export::ROUTE_NAMESPACE, '/print_job/(?P<id>.+)', array(
        'methods' => WP_REST_Server::EDITABLE,
        'callback' => array( &$this, 'webhook_create_pr_file'),
        'args' => array(
            'id' => array(
                'required' => true,
                'validate_callback' => function($param, $request, $key) {
                    return is_string($param);
                }
            ),
        ),
        'permission_callback' => function () {
            return true;
        }
    ) );

}

First, the registered endpoint doesn't have a specific permission check on the permission_callback parameter, resulting in any unauthenticated users being able to access the endpoint. Then, on the webhook_create_pr_file function itself, it will call the save_remote_file function with $remote_file_url supplied as the input parameter which can be fully controlled by the user. With this condition, users can just supply a remote PHP file that the users control and upload the files to the targeted WP server.

Note that this issue requires the AI feature (Genius) to be enabled on the WP site.

Unauthenticated SQL Injection

The underlying vulnerable code exists on the get_products_sql_attrs function:

public function get_products_sql_attrs( $attrs ) {

	$where = isset( $attrs['where'] ) ? $attrs['where'] : null;

	if( self::user_is_vendor() ) {

		$user_ids = array(get_current_user_id());

		//add fpd products from user
		$fpd_products_user_id = fpd_get_option( 'fpd_wc_dokan_user_global_products' );

		//skip if no use is set or on product builder
		if( $fpd_products_user_id !== 'none' && !(isset( $_GET['page'] ) && $_GET['page'] === 'fpd_product_builder') )
			array_push( $user_ids, $fpd_products_user_id );

		$user_ids = join( ",", $user_ids );

		$where = empty($where) ? "user_id IN ($user_ids)" : $where." AND user_id IN ($user_ids)";

	}

	//manage products filter
	if( isset($_POST['fpd_filter_users_select']) && $_POST['fpd_filter_users_select'] != "-1" ) {
		$where = "user_id=".strip_tags( $_POST['fpd_filter_users_select'] );
	}

	$attrs['where'] = $where;

	return $attrs;

} 

The function itself is registered as a handler to the fpd_get_products_sql_attrs filter:

class FPD_WC_Dokan {

	public function __construct() {

		add_action( 'admin_init', array( &$this, 'init_admin' ) );
		add_filter( 'admin_body_class', array(&$this, 'add_body_classes') );
		add_action( 'admin_menu', array( &$this, 'remove_menu_pages' ), 100 );
		add_action( 'admin_notices',  array( &$this, 'display_admin_notices' ) );

		//Dokan Dashboard
		add_filter( 'woocommerce_admin_order_actions', array( &$this, 'dashboard_orders_actions' ), 20, 2 );
		add_filter( 'dokan_get_dashboard_nav', array( &$this, 'dashboard_nav' ) );

		//Settings
		add_filter( 'fpd_woocommerce_settings', array( &$this, 'add_settings' ) );
		add_filter( 'fpd_settings_blocks', array( &$this, 'add_settings_block' ) );
		add_action( 'fpd_block_options_end', array(&$this, 'add_block_options') );

		//API filters
		add_filter( 'fpd_get_products_sql_attrs', array( &$this, 'get_products_sql_attrs' ) );
		add_filter( 'fpd_get_categories_sql_attrs', array( &$this, 'get_categories_sql_attrs' ) );

	}

The filter itself will be called from the get_products function:

public static function get_products( $attrs = array(), $type = 'catalog' ) {

	global $wpdb;

	$defaults = array(
		'cols' 		=> '*',
		'where' 	=> '',
		'order_by' 	=> '',
		'limit' 	=> null,
		'offset' 	=> null
	);

	$attrs = apply_filters( 'fpd_get_products_sql_attrs', $attrs );

	extract( array_merge( $defaults, $attrs ) );

	$products = array();
	if( fpd_table_exists(FPD_PRODUCTS_TABLE) ) {

		$where = empty($where) ? $wpdb->prepare( 'WHERE type="%s"', $type) : $wpdb->prepare( 'WHERE type="%s" AND ', $type ) . $where;
		
		if( !preg_match('/^[a-zA-Z]+\\s(ASC|DESC)$/', $order_by) )
			$order_by = '';				
		$order_by = empty($order_by) ? '' : 'ORDER BY '. $order_by;
		
		$limit = empty($limit) ? '' : $wpdb->prepare( 'LIMIT %d', $limit );
		$offset = empty($offset) ? '' : $wpdb->prepare( 'OFFSET %d', $offset );
		
		$products = $wpdb->get_results(
			"SELECT $cols FROM ".FPD_PRODUCTS_TABLE." $where $order_by $limit $offset"
		);

	}

	return $products;

}

In short, the function can be triggered by unauthenticated users. If we look at the initial get_products_sql_attrs function, we notice that the $_POST['fpd_filter_users_select'] variable is being passed to the $where, for the user_id query with only using strip_tags to sanitize the value. The strip_tags function itself is not enough to prevent SQL Injection in this case, since the function literally only strips HTML, XML, and PHP tags. Additionally, the value is constructed without quotes on the SQL query.

The injected value then will be processed in the get_products function as the $where value will be directly constructed on the $wpdb->get_results() query execution.

The patch

The vulnerability is still unpatched in the latest version known (6.4.3) at the time of publishing this article. We will update the article if the vendor releases a patch.

Conclusion

For file process, always check every process of $_FILES parameter in the plugin or theme code. Make sure to apply a check on the filename and extension before uploading the file. One of the recommendations to prevent arbitrary file uploads would be applying a whitelist on allowed file extensions.

For the SQL query process, always do a safe escape and format for the user's input before performing a query. The best practice is always to use a prepared statement and also cast each of the used variables to its intended usage, for example, always cast a variable to an integer if the intended value of the variable should be an integer value.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

17 March, 2024We found the vulnerabilities and started to create the reports.
18 March, 2024We reach out to the vendor regarding the vulnerabilities.
6 January, 2025Published the vulnerability to the Patchstack Vulnerability Database (No reply from vendor).
8 January, 2025Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Critical Vulnerabilities Found in Fancy Product Designer Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/critical-vulnerabilities-found-in-fancy-product-designer-plugin/feed/ 0
SQL Injection in WordPress – Everything You Need To Know https://patchstack.com/articles/sql-injection/ https://patchstack.com/articles/sql-injection/#respond Mon, 23 Dec 2024 10:34:00 +0000 https://patchstack.com/?p=19964 If you manage a WordPress website, you may have heard of SQL injection (also known as SQLi), a type of cyberattack. If so, you’ll probably know how ludicrously simple they are - and how devastating. Whether you’re familiar with this type of attack or you need to learn more, in this article, we’ll cover exactly […]

The post SQL Injection in WordPress – Everything You Need To Know appeared first on Patchstack.

]]>
If you manage a WordPress website, you may have heard of SQL injection (also known as SQLi), a type of cyberattack.

If so, you’ll probably know how ludicrously simple they are - and how devastating.

Whether you’re familiar with this type of attack or you need to learn more, in this article, we’ll cover exactly how to prevent SQL injection attacks in WordPress.

With WordPress powering hundreds of millions of websites worldwide, this very popularity makes such sites prime targets for cyberattacks – including SQL injection attacks. These attacks can easily compromise sensitive data, manipulate content, and even take control of the entire website.

In this guide, we'll explore essential strategies and best practices to safeguard your WordPress site against SQL injection attacks.

What is SQL?

SQL, or Structured Query Language, is a standardized language for managing and manipulating relational databases. SQL allows websites to interact with their databases efficiently, performing tasks such as retrieving, updating, inserting, and deleting data. This functionality is essential for dynamic websites, such as WordPress sites, which rely on databases to store and manage a vast array of content, user information, and settings.

For instance, when a user logs in to a WordPress site, SQL queries are executed to verify their credentials against the stored user data. Similarly, when new content is published, or existing content is updated, SQL commands are used to insert or modify the database entries accordingly.

Without SQL, managing modern websites' complex and dynamic data requirements would be cumbersome and inefficient.

Understanding the wpdb class in WordPress

WordPress relies on MySQL or MariaDB as its primary database management system. These relational database systems efficiently store, manage, and interact with the content and configuration data that powers WordPress websites.

When a WordPress site is installed, it creates a set of tables within the MySQL database, each serving a specific purpose. These tables include wp_posts for storing content, wp_users for user information, wp_comments for discussion data, and other specialized tables.

The wpdb class provides efficient methods to interact with these tables, allowing developers to perform complex queries with minimal direct SQL writing. For instance, instead of crafting raw SQL statements, developers can use methods such as $wpdb->insert(), $wpdb->update(), and $wpdb->delete() to manipulate data safely and consistently.

By using wpdb, WordPress ensures that database interactions are more straightforward and inherently protected against common security vulnerabilities like SQL injection.

It automatically handles query preparation, escaping, and sanitization, significantly mitigating potential SQL injection risks. Developers can prepare statements with placeholders, allowing WordPress to handle the safe insertion of variables into queries.

What are SQL injection (SQLi) attacks?

SQL injection is a security exploit in which malicious actors manipulate input mechanisms to insert unauthorized SQL code into database queries. This effectively tricks the application into executing unintended database operations.

Attackers can infiltrate applications through multiple entry points, not just traditional input boxes. A skilled attacker can use web forms, URL parameters, comment sections, search fields, and even HTTP headers as potential gateways for SQL injection attacks.

By carefully crafting input strings, an attacker can bypass authentication mechanisms, retrieve sensitive data, modify database contents, or even execute administrative operations on the server.

The unauthorized access gained through an SQL injection attack can then be used to retrieve, manipulate, and destroy your database, which is the heart of any WordPress website.

How do SQL injection attacks work?

An SQL injection attack exploits security vulnerabilities in poorly developed software. For example, when you log in to a website, the backend server runs an SQL statement to retrieve the password hash of a particular username. It then matches this against the hash of the value provided by the user to allow logging in.

SQL injection attacks primarily work by inserting SQL code fragments into user input boxes (such as contact forms). These SQL code fragments are then automatically concatenated (joined) with the SQL commands generated by the website and executed. A malicious actor can enter the username and password values to manipulate the SQL statement.

For instance, the attacker could add the following snippet at the end of their email:

' OR '1'='1

Additionally, an attacker can use the -- symbol to create an SQL comment. This allows attackers to strategically suppress or comment out subsequent parts of a database query, which can be used to neutralize subsequent authentication or validation checks by bypassing login or access controls.

For example, if the original query was SELECT * FROM users WHERE username='input' AND password='input', then the attacker can inject admin' -- in the input box. 

This will result in the following modified query: SELECT * FROM users WHERE username='admin' -- ' AND password='input'

This transforms the query to check only the username, bypassing password validation. The -- comments out everything after the injected input, creating a potential authentication bypass by making the password check irrelevant.

The technique exploits poorly constructed SQL queries where user inputs are directly concatenated into the query string without proper parameterization or sanitization.

This method is particularly effective against login systems, allowing unauthorized access by manipulating the query's logical structure through strategic commenting. 

Let’s consider the following code snippet to understand how an SQLi query is constructed:

select * from user_table where

username = 'admin' and

password = 'password' or 1=1 limit 1;

The third line results in an SQL query that requests admin access if EITHER the admin’s password is ‘password’ (unlikely!) OR the value of 1 is equal to 1. Since 1 does always equal 1, the SQL query will ignore the validity of the admin account’s password and grant access to the attacker at the administrative level.

This is such a simple method, but it could potentially be devastating to any business with a website that still includes this kind of vulnerability.

SQL injection on WordPress websites

WordPress sites are, by default, designed with strong security measures that safely mitigate the risk of SQL injection attacks. A dedicated security team regularly updates and maintains the core WordPress software to ensure any vulnerabilities are quickly addressed.

However, the security of a WordPress site can easily be compromised if third-party plugins are used, particularly if they are poorly coded or not regularly updated.

If a third-party plugin is vulnerable to an SQL injection attack, it can easily allow attackers to execute malicious SQL queries against your database. This can lead to severe consequences, including unauthorized access to sensitive information, data corruption, or even complete site takeover.

Such plugin vulnerabilities can completely undermine the security provided by the core WordPress software, making it critical to select third-party plugins with great care and maintain them diligently.

As a website owner, ensuring the security of your WordPress site involves protecting against SQL injection attacks, actively monitoring for vulnerabilities, and applying necessary patches.

Patchstack is an invaluable tool for helping any WordPress website swiftly identify and mitigate risks or vulnerabilities associated with third-party plugins.

Providing a 48-hour alert of any vulnerabilities ahead of public release, as well as a highly effective virtual patching system that safely addresses any risks discovered, Patchstack allows website owners to feel confident in the security of their website and the safety of their data and business’s reputation, 24 hours a day.

Common signs of an SQL injection attack

Identifying an SQL injection attack on a WordPress site can be challenging, as these attacks are often designed to be stealthy. However, several common signs may indicate your site has been compromised.

1. Unusual database errors

One of the most noticeable indicators is the appearance of unusual database errors. If you start seeing strange database error messages on your website, it could be a sign that someone is attempting to inject SQL into your database. These error messages often reveal information about your database structure that attackers can exploit further.

Additionally, unexplained entries in your database logs can indicate injection attempts. Regularly monitoring these logs for any anomalies can help in the early detection of such attacks.

2. Unexpected data appearing on the site

Another telltale sign of an SQL injection attack is the presence of unexpected data on your site. These attacks can cause unexpected changes in your website content, such as new users with administrative privileges or alterations to existing pages that you did not authorize.

Data leaks, where sensitive information that should be secure appears publicly on your website, can also result from an SQL injection exploit. These unauthorized changes and leaks often indicate that an attacker has manipulated your database.

3. Slow website performance or crashes

A sudden slowdown in website performance might be caused by an attacker using your server resources to perform an SQL injection attack. SQL injection can create heavy database queries that consume significant server resources, leading to decreased performance.

Frequent crashes are another symptom; if your site crashes more often than usual, it could be under the strain of repeated SQL injection attempts. These crashes occur because malicious SQL queries can overwhelm your database, causing it to become unresponsive or unstable.

How to protect WordPress against SQL injection attacks

To safeguard your WordPress site against SQL injection attacks, consider the following preventative measures:

  1. Keep WordPress and plugins up-to-date: Ensure that your WordPress core, themes, and plugins are always updated to the latest versions, as updates often include security patches for vulnerabilities.

Sign up for Patchstack to get notified as soon as a vulnerability is discovered in your WordPress sites.

  1. Use secure WordPress hosting: Choose a hosting provider specializing in WordPress and offering robust security features to protect against SQL injection and other threats.
  2. Restrictive access: Only grant database access to users and applications that need it. The fewer an account's privileges, the less damage an SQL injection attack can do.

Six tips for developers on writing secure PHP code for WordPress

Ensuring the security of your WordPress site starts with writing secure PHP code. Here are six essential tips to help developers protect their sites from common vulnerabilities:

1. Escape output

One of the primary ways to prevent injection attacks is by escaping output. When outputting data to the browser, always use functions such as esc_html(), esc_attr(), and esc_url().

These functions sanitize the data by converting special characters into their HTML entities, which prevents malicious scripts from being executed in the user's browser.

2. Validate and sanitize input

Never trust user input. Both validation and sanitization are crucial steps to ensure the integrity of data.

  • Validation: Validation involves checking if the input data meets specific criteria before it is processed. For example, you can verify if the input is a number, falls within a specific range, or matches a specific pattern. Functions such as is_numeric(), preg_match(), and filter_var() with appropriate filters can all be used for validation.
  • Sanitization: Sanitization involves cleaning the input data to remove or neutralize any harmful content before it is used in your application. WordPress provides a range of sanitization functions, such as sanitize_text_field(), sanitize_email(), and sanitize_key(). These functions help ensure that input data is safe for processing and storage.

3. Use prepared statements

Prepared statements ensure that SQL queries are correctly escaped, and that user inputs are treated as data rather than executable code. This method separates SQL logic from user input, making it much harder for attackers to inject malicious SQL code.

Here's an example of how to use the prepared statements:

$post_id = 10;

$metakey = sanitize_key('Patchstack Security Key');

$metavalue = wp_kses_post('WordPress security insights');

$prepared_statement = $wpdb->prepare(

   "INSERT INTO $wpdb->postmeta 

   ( post_id, meta_key, meta_value ) 

   VALUES ( %d, %s, %s )",

   $post_id,

   $metakey,

   $metavalue

);

$wpdb->query($prepared_statement);

In this example:

  • The code uses WordPress's $wpdb global database object to securely insert metadata into the wp_postmeta table.
  • $wpdb->prepare() creates a parameterized SQL query, which prevents SQL injection by separating SQL logic from user-supplied data.
  • WordPress sanitization functions (sanitize_text_field() and wp_kses_post()) are used to clean and validate input data before database insertion.
  • The prepared statement uses type-specific placeholders (%d for integers, %s for strings) to ensure data type safety and protect against potential SQL injection attacks.

4. Use parameterized queries

Parameterized queries use placeholders in the SQL statement for input values, which are then replaced with actual values at runtime. This method prevents SQL injection attacks, improves query performance, and makes code easier to read and maintain.

In SQL Server, you can declare parameters using the DECLARE statement, define parameters in a stored procedure, or manually prepare an SQL statement. 

To use parameters in your SQL queries, you can reference them with the @ symbol followed by the parameter name. For example:

DECLARE @SubscriberID INT;

SET @SubscriberID = 1001;

SELECT * FROM Subscribers

WHERE SubscriberID = @SubscriberID;

In the following example, @SubscriberID is a parameter that allows you to pass an integer value to the stored procedure.

CREATE PROCEDURE GetSubscriberByID

    @SubscriberID INT

AS

BEGIN

    -- SQL query using the parameter

    SELECT * FROM Subscribers WHERE SubscriberID = @SubscriberID;

END;

After creating the statement, you can call it using PHP in your WordPress application. For example, consider the following code snippet:

<?php

function {plugin_prefix}_call_stored_procedure() {

    global $wpdb;

    $wpdb->query('CALL GetSubscriberByID(42)');

    $subscribers = $wpdb->get_results(null, ARRAY_A);

    // Error handling 

    if ($wpdb->last_error) {

        error_log('Stored Procedure Error: ' . $wpdb->last_error);

        return null;

    }

    return $result;

}

These methods allow you to dynamically create new SQL statements without risking an SQLi attack. When you use these methods, all the values are automatically escaped when the statement is executed. This prevents SQL injection since the user input is treated as data, not code.

5. Remove Escape Characters

The wp_unslash() function is a built-in WordPress security function that removes potential escape characters from input data. Stripping out backslashes that might be used to manipulate SQL queries helps prevent SQL injection and other security vulnerabilities.

It neutralizes potential attack vectors where malicious actors could inject additional SQL code. The function provides an additional layer of input sanitization by ensuring that escape characters cannot be used to break out of query parameters or modify query logic. 

Unlike deprecated methods such as strip slashes (), wp_unslash() is specifically designed for WordPress environments and offers more robust protection against complex injection techniques. 

6. Use nonce fields

Nonces (numbers used once) are a WordPress security feature to protect against cross-site request forgery (CSRF) attacks. Using nonces in your forms can verify that the submitted data comes from your site. When creating a form, generate a nonce field using wp_nonce_field() and verify it with check_admin_referer() or check_ajax_referer() upon form submission.

7. Add CAPTCHA

Implementing CAPTCHA on forms helps prevent bots from automating submissions. CAPTCHAs require users to complete a simple test that is easy for humans but difficult for automated scripts. Various plugins are available to add CAPTCHA to your WordPress forms, ensuring that only legitimate users can submit data.

Final thoughts

SQL injection attacks seriously threaten WordPress websites, potentially allowing attackers to manipulate your database, steal sensitive information, and compromise your site's security. Protecting your website against SQL injection is crucial, but it is only one part of the equation.

To ensure comprehensive security, you must monitor for vulnerabilities and apply patches promptly. Hardening your website by implementing additional security measures and blocking bad-bot traffic is essential in creating a robust defense.

This is where Patchstack comes in.

Patchstack offers a robust suite of tools to protect your WordPress site from SQL injection attacks and other vulnerabilities. With Patchstack, you can easily monitor your site for security issues, receive timely alerts about new vulnerabilities, and apply patches seamlessly. Additionally, Patchstack helps you harden your site and block malicious traffic, keeping your WordPress installation secure and running smoothly.

Don't wait for an attack to happen – Sign up for Patchstack and secure your WordPress site today.

The post SQL Injection in WordPress – Everything You Need To Know appeared first on Patchstack.

]]>
https://patchstack.com/articles/sql-injection/feed/ 0
Multiple Critical Vulnerabilities Patched in WPLMS and VibeBP Plugins https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wplms-and-vibebp-plugins/ https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wplms-and-vibebp-plugins/#respond Mon, 23 Dec 2024 10:00:00 +0000 https://patchstack.com/?p=20012 This blog post is about the WPLMS and VibeBP vulnerabilities. If you're a WPLMS and VibeBP user, please update the plugin to at least version 1.9.9.5.3 and 1.9.9.7.7 respectively. About the WPLMS and VibeBP Plugins Both of the plugins are required plugins for the WPLMS theme, which has over 28,000 sales. The theme itself is […]

The post Multiple Critical Vulnerabilities Patched in WPLMS and VibeBP Plugins appeared first on Patchstack.

]]>

WPLMS

Unauthenticated Arbitrary File Upload

28k
CVSS 10.0

WPLMS

Subscriber+ Arbitrary File Upload

28k
CVSS 9.9

WPLMS

Sutedent+ Arbitrary File Upload

28k
CVSS 9.9

WPLMS

Unauthenticated Privilege Escalation

28k
CVSS 9.8

WPLMS

Subscriber+ Privilege Escalation

28k
CVSS 8.8

WPLMS

Unauthenticated SQL Injection

28k
CVSS 9.3

WPLMS

Subscriber+ SQL Injection

28k
CVSS 8.5

VibeBP

Unauthenticated Privilege Escalation

28k
CVSS 9.8

VibeBP

Unauthenticated SQL Injection

28k
CVSS 9.3

VibeBP

Subscriber+ SQL Injection

28k
CVSS 8.5

This blog post is about the WPLMS and VibeBP vulnerabilities. If you're a WPLMS and VibeBP user, please update the plugin to at least version 1.9.9.5.3 and 1.9.9.7.7 respectively.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the WPLMS and VibeBP Plugins

Both of the plugins are required plugins for the WPLMS theme, which has over 28,000 sales. The theme itself is a popular premium LMS theme for WordPress. This theme is developed by VibeThemes.

This premium LMS theme is used for creating online courses, managing students, and selling educational content. It integrates with WooCommerce, BuddyPress, and more, offering features like quizzes, certificates, and instructor dashboards.

The security vulnerabilities

The WPLMS and VibeBP plugins suffer from multiple critical vulnerabilities.

The first vulnerability is Arbitrary file upload. This vulnerability allows unauthenticated and authenticated users to upload arbitrary files to the server. In the worst-case scenario, this could lead to Remote Code Execution (RCE) when the users upload PHP files.

The second vulnerability is Privilege Escalation. This vulnerability allows unauthenticated and authenticated users with minimum roles such as Subscriber role to register as any role on an impacted website, including privileged roles such as Administrator. In the worst-case scenario, this could lead to an attacker's full takeover of the website and malicious code installed on the server.

The third vulnerability is SQL Injection. This vulnerability allows unauthenticated and authenticated users with minimum roles such as the Subscriber role to execute malicious SQL queries and gain information leaks from the database.

All of the mentioned vulnerabilities in this article are patched on different versions of WPLMS and VibeBP plugin. We strongly recommend updating to at least version 1.9.9.5.3 for WPLMS and version 1.9.9.7.7 for the VibeBP plugin to ensure protection from all of the critical vulnerabilities.

WPLMS: Unauthenticated Arbitrary File Upload

This vulnerability is assigned CVE-2024-56046. The vulnerable code exists in the wplms_form_uploader_plupload function, found in includes/vibe-shortcodes/shortcodes.php:

function wplms_form_uploader_plupload(){
  check_ajax_referer('wplms_form_uploader_plupload');

  if (empty($_FILES) || $_FILES['file']['error']) {
      die('{"OK": 0, "info": "Failed to move uploaded file."}');
  }
  $chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
  $chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
  $fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : $_FILES["file"]["name"];

  $upload_dir_base = wp_upload_dir();
  $folderPath = $upload_dir_base['basedir']."/wplms_form_uploader";
  if(function_exists('is_dir') && !is_dir($folderPath)){
      if(function_exists('mkdir')) 
          mkdir($folderPath, 0755, true) || chmod($folderPath, 0755);
  }
  $filePath = $folderPath."/$fileName";

  // Open temp file
  if($chunk == 0) 
      $perm = "wb" ;
  else 
      $perm = "ab";

  $out = @fopen("{$filePath}.part",$perm );

  if ($out) {
    // Read binary input stream and append it to temp file
    $in = @fopen($_FILES['file']['tmp_name'], "rb");
    
    if ($in) {
      while ($buff = fread($in, 4096))
        fwrite($out, $buff);
    } else
      die('{"OK": 0, "info": "Failed to open input stream."}');
    
    @fclose($in);
    @fclose($out);
    
    @unlink($_FILES['file']['tmp_name']);
  } else
    die('{"OK": 0, "info": "Failed to open output stream."}');

  // Check if file has been uploaded
  if (!$chunks || $chunk == $chunks - 1) {
    // Strip the temp .part suffix off
      rename("{$filePath}.part", $filePath);
      
  }
  die('{"OK": 1, "info": "Upload successful."}');
  exit;
}

This function is called by the wp_ajax_nopriv_wplms_form_uploader_plupload action and can be accessed by an unauthenticated user. There is no proper check on the $_FILES and $_REQUEST["name"] which is used as the uploaded file name on the server, resulting in arbitrary file upload on the server.

WPLMS: Subscriber+ Arbitrary File Upload

This vulnerability is assigned CVE-2024-56050. The vulnerable code exists in the wp_ajax_zip_upload function, found in includes/vibe-shortcodes/upload_handler.php:

function wp_ajax_zip_upload(){
	$arr = array();
	
	$file = $_FILES['uploadedfile']['tmp_name'];
	$dir = explode(".",$_FILES['uploadedfile']['name']);
	$dir[0] = str_replace(" ","_",$dir[0]);
	$target = $this->getUploadsPath().$dir[0];
	$index = count($dir) -1;

	if (!isset($dir[$index]) || $dir[$index] != "zip")
		$arr[0] = __('The Upload file must be zip archive','wplms');
	else{
		while(file_exists($target)){
			$r = rand(1,10);
			$target .= $r;
			$dir[0] .= $r;
		}
		if (!empty($file))
			$arr = $this->extractZip($file,$target,$dir[0]);
		else
			$arr[0] = __('File too big','wplms');
	}
		echo json_encode($arr);
	die();
}

This function is called by the wp_ajax_zip_upload action and can be accessed by any authenticated user such as a Subscriber role user. In this function, users are allowed to upload a ZIP file and it will be processed by $this->extractZip function:

function extractZip($fileName,$target,$dir){
	$arr = array();
	$zip = new ZipArchive;
	$res = $zip->open($fileName);
	if ($res === TRUE) {
		$zip->extractTo($target);
		$zip->close();
		$file = $this->getFile($target);
		;
	if($file){
			$arr[0] = 'uploaded'; 
			$arr[1] = $this->getUploadsUrl().$dir."/".$file; 
			$arr[2] = $dir;
			$arr[3] =$file;
			$arr[4] = $this->getUploadsPath().$dir; 
		}else{
			$arr[0] = __('Please upload zip file, Index.html file not found in package','wplms').$target.print_r($file);
			$this->rrmdir($target);
		}
	}else{
	$arr[0] = __('Upload failed !','wplms');;
	}
	return  $arr;
}

The extractZip function itself simply extracted all of the files inside of the uploaded ZIP file to $target location which users can control via $_FILES['uploadedfile']['name'] value on the wp_ajax_zip_upload function. Since there is no pre-check on the files inside of the ZIP, users can just host a PHP file inside of the ZIP file and upload the ZIP file to the server.

WPLMS: Student+ Arbitrary File Upload

This vulnerability is assigned CVE-2024-56052. The vulnerable code exists in the wplms_assignment_plupload function, found in includes/assignments/assignments.php:

function wplms_assignment_plupload(){
  check_ajax_referer('wplms_assignment_plupload');
  if(!is_user_logged_in())
      die('user not logged in');

  $user_id = get_current_user_id();
  
  if (empty($_FILES) || $_FILES['file']['error']) {
    die('{"OK": 0, "info": "Failed to move uploaded file."}');
  }

  $chunk = isset($_REQUEST["chunk"]) ? intval($_REQUEST["chunk"]) : 0;
  $chunks = isset($_REQUEST["chunks"]) ? intval($_REQUEST["chunks"]) : 0;
  $fileName = isset($_REQUEST["name"]) ? $_REQUEST["name"] : $_FILES["file"]["name"];
  
  $upload_dir_base = wp_upload_dir();
  $assignment_id = $_POST['assignment_id'];
  $folderPath = $upload_dir_base['basedir']."/wplms_assignments_folder/".$user_id.'/'.$assignment_id;
  if(function_exists('is_dir') && !is_dir($folderPath)){
      if(function_exists('mkdir')) 
          mkdir($folderPath, 0755, true) || chmod($folderPath, 0755);
  }


  $filePath = $folderPath."/$fileName";
    /*if(function_exists('file_exists') && file_exists($filePath)){
      echo __(' Chunks upload error ','wplms'). $fileName.__(' already exists.Please rename your file and try again ','wplms');
      die();
    }*/
  // Open temp file
  if($chunk == 0) $perm = "wb" ;
  else $perm = "ab";

  $out = @fopen("{$filePath}.part",$perm );

  if ($out) {
    // Read binary input stream and append it to temp file
    $in = @fopen($_FILES['file']['tmp_name'], "rb");
    
    if ($in) {
      while ($buff = fread($in, 4096))
        fwrite($out, $buff);
    } else
      die('{"OK": 0, "info": "Failed to open input stream."}');
    
    @fclose($in);
    @fclose($out);
    
    @unlink($_FILES['file']['tmp_name']);
  } else
    die('{"OK": 0, "info": "Failed to open output stream."}');
    
    
  // Check if file has been uploaded
  if (!$chunks || $chunk == $chunks - 1) {
    // Strip the temp .part suffix off
      rename("{$filePath}.part", $filePath);
      
  }
  die('{"OK": 1, "info": "Upload successful."}');
  exit;
}

This function is called by the wp_ajax_wplms_assignment_plupload action. Although the function has a nonce check and is_user_logged_in check, the function can still be accessed by users with a Student role. There is no proper check on the $_FILES and $_REQUEST["name"] which is used as the uploaded file name on the server, resulting in arbitrary file upload on the server.

WPLMS: Unauthenticated Privilege Escalation

This vulnerability is assigned CVE-2024-56043. The vulnerable code exists in the wplms_register_user function, found in includes/vibe-shortcodes/ajaxcalls.php:

function wplms_register_user(){
    if ( !isset($_POST['security']) || !wp_verify_nonce($_POST['security'],'bp_new_signup') || !isset($_POST['settings'])){
        echo '<div class="message">'.__('Security check Failed. Contact Administrator.','wplms').'</div>';
        die();
    }
    $flag = 0;
    $settings = json_decode(stripslashes($_POST['settings']));
    if(empty($settings)){
        $flag = 1; 
    }
------------- CUT HERE -------------

    $user_args = $user_fields = $save_settings = array();

    if(empty($flag)){

------------- CUT HERE -------------

        foreach($settings as $setting){

            if(!empty($setting->id)){
                $settings2[] = $setting->id;
                if($setting->id == 'signup_username'){
                    $user_args['user_login'] = $setting->value;
                }else if($setting->id == 'signup_email'){
                    $user_args['user_email'] = $setting->value;
                }else if($setting->id == 'signup_password'){
                    $user_args['user_pass'] = $setting->value;
                }else{
                    if(strpos($setting->id,'field') !== false){

                        $f = explode('_',$setting->id);
                        $field_id = $f[1]; 
                        if(strpos($field_id, '[')){ //checkbox
                            $v = str_replace('[','',$field_id);
                            $v = str_replace(']','',$v);
                            $field_id = $v;
                            if(is_Array($user_fields[$field_id]['value'])){
                                $user_fields[$field_id]['value'][] = $setting->value;
                            }else{
                                $user_fields[$field_id] = array('value'=>array($setting->value));
                            }
                        }else{
                            if(is_numeric($field_id) && !isset($f[2])){
                                $user_fields[$field_id] = array('value'=>$setting->value);
                            }else{
                                if(in_array($f[2],array('day','month','year'))){
                                    $user_fields['field_' . $field_id . '_'.$f[2]] = $setting->value;
                                }else{
                                    $user_fields[$field_id]['visibility']=$setting->value;    
                                }
                            }
                        }
                        
                    }else{
                        if(isset($form_settings[$setting->id])){
                        
                            $form_settings[$setting->id] = 0; // use it for empty check 
                            if($setting->id=='default_role'){
                                $save_settings[$setting->id]=$setting->value;
                                $user_args['role'] = $setting->value;
                            }
                            if($setting->id=='member_type'){
                                $save_settings[$setting->id]=$setting->value;
                                $member_type=$setting->value;
                            }
                            if($setting->id=='wplms_user_bp_group'){
                                if(in_array($setting->value,$reg_form_settings['settings']['wplms_user_bp_group']) || $reg_form_settings['settings']['wplms_user_bp_group'] === array('enable_user_select_group')){
                                    $save_settings[$setting->id]=$setting->value;
                                    $wplms_user_bp_group = $setting->value;
                                }else{
                                    echo '<div class="message_wrap"><div class="message error">'._x('Invalid Group selection','error message when group is not valid','wplms').'<span></span></div></div>';
                                    die();
                                }
                                
                            }
                        }
                        
                    }
                }
            }
        }
        if(!in_array('wplms_user_bp_group', $settings2)){
            if(!empty($reg_form_settings['settings']['wplms_user_bp_group']) && is_array($reg_form_settings['settings']['wplms_user_bp_group']) && $reg_form_settings['settings']['wplms_user_bp_group'] !== array('enable_user_select_group') && count($reg_form_settings['settings']['wplms_user_bp_group'])==1){
                $wplms_user_bp_group = $reg_form_settings['settings']['wplms_user_bp_group'][0];
            }
        }
    }

------------- CUT HERE -------------

    /*
    FORM SETTINGS
    */
    if(empty($form_settings['hide_username'])){
        $user_args['user_login'] = $user_args['user_email'];
    }
    $user_id = 0;
    if(empty($form_settings['skip_mail'])){
        $user_id = wp_insert_user($user_args);

------------- CUT HERE -------------

This function is called by the wp_ajax_nopriv_wplms_register_user action and is used to process registration form submission. First, users are able to arbitrarily set the $settings object. Then, there is a $user_args object which is used to construct user data for registration. The code will assign $user_args['role'] with $setting->value which is simply a value from $settings['default_role']. Lastly, the function will register the user using wp_insert_user($user_args). Since there is no proper check on the $user_args['role'] value, users can just supply arbitrary roles on the registration process and can escalate their privilege to any role including the Administrator role.

WPLMS: Subscriber+ Privilege Escalation

This vulnerability is assigned CVE-2024-56048. The vulnerable code exists in the update_license_key function, found in includes/vibe-customtypes/includes/musettings.php:

function update_license_key(){
	if ( !isset($_POST['security']) || !wp_verify_nonce($_POST['security'],'security')){
			_e('Security check Failed. Contact Administrator.','wplms');
		die();
	}
	if(empty($_POST['addon']) || empty($_POST['key'])){
		_e('Unable to update key.','wplms');
		die();
	}
	update_option($_POST['addon'],$_POST['key']);
	echo apply_filters('wplms_addon_license_key_updated',__('Key Updated.','wplms'));
	die();
}

This function is called by the wp_ajax_vibe_update_license_key action and can be accessed by any authenticated users such as a Subscriber role since the nonce can be fetched from a Subscriber role account and there is no proper permission check on the function. Since there is no restriction on the $_POST['addon'] and $_POST['key'] variables passed to the update_option function, this results in an Arbitrary Option Update and allows the user to set any of the site options to any value. With this, users can simply enable the users_can_register option and then set the default_role option to any role such as the Administrator role. This will result in open registration on the WP site and the assigned role upon registration is Administrator role.

WPLMS: Unauthenticated SQL Injection

This vulnerability is assigned CVE-2024-56042. Originally, we found around 20+ affected variables and code for this specific vulnerability. One of the vulnerable codes exists in the get_instructor_commissions_chart function, found in includes/vibe-course-module/includes/api/v3/class-api-commissions.php:

function get_instructor_commissions_chart($request){

	$user_id = $request->get_param('id');
	$course_id =$request->get_param('course_id');	
	$date_start = $request->get_param('date_start');	
	$date_end = $request->get_param('date_end');
	$currency = $request->get_param('currency');
------------ CUT HERE ------------

	$and_where = '';
	$start_date = '';
	$end_date = '';
	$group_by = ' GROUP BY select_parameter';
	$select = 'MONTH(activity.date_recorded) as select_parameter';

	if(!empty($course_id)){
		$and_where .= " AND activity.item_id = $course_id ";
	}else{
		
------------ CUT HERE ------------
	}
	if(!empty($currency)) {
		$and_where .= " AND meta2.meta_value = '".$currency."' ";
	}

------------ CUT HERE ------------
	global $wpdb;
	global $bp;
	$results = $wpdb->get_results( "
									SELECT ".$select.", sum(meta.meta_value) as commission
									FROM {$bp->activity->table_name} AS activity 
									LEFT JOIN {$bp->activity->table_name_meta} as meta ON activity.id = meta.activity_id
									LEFT JOIN {$bp->activity->table_name_meta} as meta2 ON activity.id = meta2.activity_id
									WHERE     activity.component     = 'course'
									AND     activity.type     = 'course_commission'
									AND     activity.user_id     = {$user_id}
									AND     meta.meta_key   LIKE '_commission%'
									AND     meta2.meta_key   LIKE '_currency%'
									".$and_where."
									".$group_by,ARRAY_A);
------------ CUT HERE ------------
}

This function is handling a REST endpoint of /wp-json/wplms/v1/commissions/instructor/<ID>/chart. Let's take a look at the REST endpoint registration process:

register_rest_route( $this->namespace, '/instructor/(?P<id>\d+)/chart/', array(
	array(
		'methods'             =>  WP_REST_Server::READABLE,
		'callback'            =>  array( $this, 'get_instructor_commissions_chart' ),
		'permission_callback' => array( $this, 'commissions_request_validate' ),
		'args'                     	=>  array(
			'id'                       	=>  array(
				'validate_callback'     =>  function( $param, $request, $key ) {
											return is_numeric( $param );
										}
			),
		),
	),
));

The REST endpoint itself has a permission check using commissions_request_validate:

function commissions_request_validate($request){

	$user_id = $request->get_param('id');
	
	$user = get_userdata( $user_id );
	if ( $user === false ) {
		return false;
	} else {
		return true;
	}
------------ CUT HERE ------------

As we can see, unauthenticated users can simply bypass the check by providing any valid user ID value. Back to the get_instructor_commissions_chart function, there is no proper escaping process on $course_id and $currency resulting in SQL Injection. Note that we are not able to inject the $user_id variable because the value is coming from $request->get_param('id') which only allows valid numeric values via the validate_callback args.

WPLMS: Subscriber+ SQL Injection

This vulnerability is assigned CVE-2024-56047. Originally, we found around 10+ affected variables and code for this specific vulnerability. One of the vulnerable codes exists in the search_users_in_chat function, found in includes/vibe-course-module/includes/api/v3/class-api-user-controller.php:

function search_users_in_chat($request){
	global $wpdb;
	$user_initials = $request->get_param('user_initials');
	$results = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}users WHERE `user_nicename` LIKE '%{$user_initials}%'", ARRAY_A );

	$return = array('status'=>1,'message'=>'','users'=>array());
	if(!empty($results)){
		foreach($results as $result){
			$return['users'][]=apply_filters('wplms_api_search_users_in_chat',array(
				'name'=> bp_core_get_user_displayname($result['ID']),
				'id'=> intval($result['ID']),
				'image'=> bp_core_fetch_avatar(array('item_id' => $result['ID'],'type'=>'thumb', 'html' => false)),
				'type'=> (user_can(intval($result['ID']),'manage_options')?_x('Administrator','Chat search result user type','wplms'):(user_can($result['ID'],'edit_posts')?_x('Instructor','Chat search result user type','wplms'):_x('Student','Chat search result user type','wplms')))
			));
		}
	}else{
		$return = array('status'=> 0,'message'=>_x('No user found !','Chat search result','wplms'),'users'=>array());
	}
	return new WP_REST_Response($return, 200);
}

This function handles a REST endpoint of /wp-json/wplms/v2/user/alluser and can be accessed by any authenticated users. We are able to inject SQL query to $user_initials which is constructed from $request->get_param('user_initials') since there is no proper escaping.

VibeBP: Unauthenticated Privilege Escalation

This vulnerability is assigned CVE-2024-56040. The vulnerable code exists in the vibebp_register_user function, found in includes/class.ajax.php:

function vibebp_register_user(){
    if ( !isset($_POST['security']) || !wp_verify_nonce($_POST['security'],'bp_new_signup') || !isset($_POST['settings'])){
        echo '<div class="message">'.__('Security check Failed. Contact Administrator.','wplms').'</div>';
        die();
    }
    $flag = 0;
    $settings = json_decode(stripslashes($_POST['settings']));
    if(empty($settings)){
        $flag = 1; 
    }

------------- CUT HERE -------------

    $user_args = $user_fields = $save_settings = array();

    if(empty($flag)){

------------- CUT HERE -------------

        foreach($settings as $setting){

            if(!empty($setting->id)){
                $settings2[] = $setting->id;
                if($setting->id == 'signup_username'){
                    $user_args['user_login'] = $setting->value;
                }else if($setting->id == 'signup_email'){
                    $user_args['user_email'] = $setting->value;
                }else if($setting->id == 'signup_password'){
                    $user_args['user_pass'] = $setting->value;
                }else{
                    if(strpos($setting->id,'field') !== false){

                        $f = explode('_',$setting->id);
                        $field_id = $f[1]; 
                        if(strpos($field_id, '[')){ //checkbox
                            $v = str_replace('[','',$field_id);
                            $v = str_replace(']','',$v);
                            $field_id = $v;
                            if(is_Array($user_fields[$field_id]['value'])){
                                $user_fields[$field_id]['value'][] = $setting->value;
                            }else{
                                $user_fields[$field_id] = array('value'=>array($setting->value));
                            }
                        }else{
                            if(is_numeric($field_id) && !isset($f[2])){
                                $user_fields[$field_id] = array('value'=>$setting->value);
                            }else{
                                if(in_array($f[2],array('day','month','year'))){
                                    $user_fields['field_' . $field_id . '_'.$f[2]] = $setting->value;
                                }else{
                                    $user_fields[$field_id]['visibility']=$setting->value;    
                                }
                            }
                        }
                        
                    }else{
                        if(isset($form_settings[$setting->id])){
                        
                            $form_settings[$setting->id] = 0; // use it for empty check 
                            if($setting->id=='default_role'){
                                $save_settings[$setting->id]=$setting->value;
                                $user_args['role'] = $setting->value;
                            }
                            if($setting->id=='member_type'){
                                $save_settings[$setting->id]=$setting->value;
                                $member_type=$setting->value;
                            }
                            if($setting->id=='vibebp_user_bp_group'){
                                if(in_array($setting->value,$reg_form_settings['settings']['vibebp_user_bp_group']) || $reg_form_settings['settings']['vibebp_user_bp_group'] === array('enable_user_select_group')){
                                    $save_settings[$setting->id]=$setting->value;
                                    $vibebp_user_bp_group = $setting->value;
                                }else{
                                    echo '<div class="message_wrap"><div class="message error">'._x('Invalid Group selection','error message when group is not valid','wplms').'<span></span></div></div>';
                                    die();
                                }
                                
                            }
                        }
                        
                    }
                }
            }
        }
        if(!in_array('vibebp_user_bp_group', $settings2)){
            if(!empty($reg_form_settings['settings']['vibebp_user_bp_group']) && is_array($reg_form_settings['settings']['vibebp_user_bp_group']) && $reg_form_settings['settings']['vibebp_user_bp_group'] !== array('enable_user_select_group') && count($reg_form_settings['settings']['vibebp_user_bp_group'])==1){
                $vibebp_user_bp_group = $reg_form_settings['settings']['vibebp_user_bp_group'][0];
            }
        }
    }



    $user_args = apply_filters('vibebp_register_user_args',$user_args);
    

    //hook for validations externally
    do_action('vibebp_custom_registration_form_validations',$name,$settings,$all_form_settings,$user_args);
    do_action('wplms_custom_registration_form_validations',$name,$settings,$all_form_settings,$user_args);

    /*
    RUN CONDITIONAL CHECKS
    */
    $check_filter = filter_var($user_args['user_email'], FILTER_VALIDATE_EMAIL); // PHP 5.3
    if(empty($user_args['user_email']) || empty($user_args['user_pass']) || empty($check_filter)){
        echo '<div class="message_wrap"><div class="message error">'._x('Invalid Email/Password !','error message when registration form is empty','wplms').'<span></span></div></div>';
        die();
    }

    //Check if user exists
    if(!isset($user_args['user_email']) || email_exists($user_args['user_email'])){
        echo '<div class="message_wrap"><div class="message error">'._x('Email already registered.','error message','wplms').'<span></span></div></div>';
        die();
    }

    //Check if user exists
    if(!isset($user_args['user_login'])){

        $user_args['user_login'] = $user_args['user_email'];
        if(email_exists($user_args['user_login'])){
            echo '<div class="message_wrap"><div class="message error">'._x('Username already registered.','error message','wplms').'<span></span></div></div>';
            die();
        }
    }elseif (username_exists($user_args['user_login'])){
        echo '<div class="message_wrap"><div class="message error">'._x('Username already registered.','error message','wplms').'<span></span></div></div>';
        die();
    }
    
------------- CUT HERE -------------

    /*
    FORM SETTINGS
    */
    if(empty($form_settings['hide_username'])){
        $user_args['user_login'] = $user_args['user_email'];
    }
    $user_id = 0;
    if(empty($form_settings['skip_mail'])){
        $user_id = wp_insert_user($user_args);

------------- CUT HERE -------------

The condition of this vulnerability is quite similar to the Unauthenticated Privilege Escalation case in the WPLMS plugin. This function is called by the wp_ajax_nopriv_vibebp_register_user action and is used to process registration form submission. First, users are able to arbitrarily set the $settings object. Then, there is a $user_args object which is used to construct user data for registration. The code will assign $user_args['role'] with $setting->value which is simply a value from $settings['default_role']. Lastly, the function will register the user using wp_insert_user($user_args). Since there is no proper check on the $user_args['role'] value, users can just supply arbitrary roles on the registration process and can escalate their privilege to any role including the Administrator role.

VibeBP: Unauthenticated SQL Injection

This vulnerability is assigned CVE-2024-56039. Originally, we found around 3 affected variables and code for this specific vulnerability. One of the vulnerable codes exists in the get_avatar function, found in includes/buddypress/class-api-settings-controller.php:

function get_avatar($request){

	$body = json_decode($request->get_body(),true);
	$body = vibebp_recursive_sanitize_text_field($body);
	$name = '';
	$avatar= '';
	$key='';
	$type = '';
	if(!empty($body['type'])){$type=$body['type'];}
	switch($type){
		case 'friends':
		
		$key = 'user_'.$body['ids']['item_id'];
		$avatar = bp_core_fetch_avatar(array(
			'item_id' => (int)$body['ids']['item_id'],
			'object'  => 'user',
			'type'=>'thumb',
			'html'    => false
		));
		$name = bp_core_get_user_displayname($body['ids']['item_id']);
		
			
		break;
		case 'group':
			$key = 'group_'.$body['ids']['item_id'];
			$avatar = bp_core_fetch_avatar(array(
				'item_id' => (int)$body['ids']['item_id'],
				'object'  => 'group',
				'type'=>'thumb',
				'html'    => false
			));
			global $wpdb,$bp;
			$name = $wpdb->get_var("SELECT name from {$bp->groups->table_name} WHERE id=".$body['ids']['item_id']);
------------- CUT HERE -------------

This function handles the REST endpoint of /wp-json/vbp/v1/avatar. Let's take a look at the REST endpoint registration process:

register_rest_route( $this->namespace, '/avatar', array(
	array(
		'methods'             =>  'POST',
		'callback'            =>  array( $this, 'get_avatar'),
		'permission_callback' => array( $this, 'get_client_permissions' ),
	),
));

The REST endpoint itself has a permission check using get_client_permissions:

function get_client_permissions($request){
	
	$client_id = $request->get_param('client_id');
	if($client_id == vibebp_get_setting('client_id')){
		return true;
	}

	return $this->get_settings_permissions($request);
	
}

The vibebp_get_setting('client_id') value itself can be fetched by unauthenticated users when they try to complete a checkout process in WooCommerce. Back to the get_avatar function, there is no proper escaping process on $body['ids']['item_id'] resulting in SQL Injection.

VibeBP: Subscriber+ SQL Injection

This vulnerability is assigned CVE-2024-56041. Originally, we found around 2 affected variables and code for this specific vulnerability. One of the vulnerable codes exists in the remove_message_label function, found in includes/buddypress/class-api-messages-controller.php:

function remove_message_label($request){
	$body = json_decode($request->get_body(),true);
	$body = vibebp_recursive_sanitize_text_field($body);
	$labels = get_user_meta($this->user->id,'vibebp_message_labels',true);
	if(!empty($labels)){
		$remove = 0;
		foreach($labels as $k=>$l){
			if($l['slug'] === $body['slug']){
					$remove = $k;
					break;
			}
		}
		$label_key = 'vibebp_label_'.$this->user->id;
		$slug = $body['slug'];
		global $wpdb,$bp;
		$labels_count = $wpdb->get_results("DELETE FROM {$bp->messages->table_name_meta} WHERE meta_key = '$label_key' AND meta_value = '$slug'");
		unset($labels[$remove]);
		update_user_meta($this->user->id,'vibebp_message_labels',$labels);
	}

	return new WP_REST_Response( array('status'=>1,'labels'=>$labels,'message'=>_x('Label removed.','message','vibebp')), 200 ); 
}

This function handles the REST endpoint of /wp-json/vbp/v1/messages/label/remove. The endpoint can be accessed by any authenticated users such as Subscriber role users. Since there is no proper escaping process on $slug, users are able to perform SQL Injection.

NOTE: Originally, we managed to report 18 different vulnerabilities to both WPLMS and VibeBP plugin. We only cover some of the critical vulnerabilities in this article.

The Patch

For the Arbitrary File Upload vulnerabilities, the vendor applies a patch to limit which file can be uploaded using a check on the file name and types. On some of the issues of Arbitrary File Upload, vendors also implement additional permission checks to the affected functions or remove the affected code.

For the Privilege Escalation vulnerabilities, the vendor applies a patch to limit which roles a user can register as. The patch implements a change where the user will be assigned a default role configured from the registration form setting. For Privilege Escalation via Arbitrary Option Update, the vendor implements an additional permission check on the function and applies a whitelist check on the option name that can be updated.

For SQL Injection vulnerabilities, the vendor applies a proper escaping to all of the reported variables and code.

Conclusion

The vulnerabilities discussed here highlight the importance of secure file upload, registration, and SQL query processes.

In the context of registration, care needs to be taken to ensure that users can only register with specifically acceptable roles. When implementing a custom registration portal, we recommend utilizing allowlists of only specifically allowed roles or using a default role on the registration process.

In the context of the file upload process, always implement both file name and file type checks and only a allow specific set of file types to be uploaded by the users. In this case, we recommend applying a whitelist check instead of a blacklist check.

In the context of the SQL query process, always make sure that the controlled user's input is properly escaped when constructed into an SQL query. The best practice is using a prepared statement with a proper implementation.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

31 March, 2024Vulnerabilities found, reports generated. The vendor was notified about all of the vulnerabilities.
1-2 April, 2024The vendor submits the first proposed patch. We review this patch and continue to work with the vendor to advise on how they can best remediate all of the vulnerabilities.
16-18 October, 2024The vendor submits the second proposed patch. Some of the vulnerabilities are patched and others are still waiting for a proper patch.
18-19 November, 2024The vendor submits the last proposed patch. We are able to validate that all of the reported vulnerabilities have been fixed.
18 December, 2024We published the vulnerability to the Patchstack Vulnerability Database.
23 December, 2024Security advisory article released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Multiple Critical Vulnerabilities Patched in WPLMS and VibeBP Plugins appeared first on Patchstack.

]]>
https://patchstack.com/articles/multiple-critical-vulnerabilities-patched-in-wplms-and-vibebp-plugins/feed/ 0
Unpatched SQL Injection Vulnerability in TI WooCommerce Wishlist Plugin https://patchstack.com/articles/unpatched-sql-injection-vulnerability-in-ti-woocommerce-wishlist-plugin/ https://patchstack.com/articles/unpatched-sql-injection-vulnerability-in-ti-woocommerce-wishlist-plugin/#respond Wed, 25 Sep 2024 07:57:36 +0000 https://patchstack.com/?p=19075 Critical SQL Injection Alert: The TI WooCommerce Wishlist plugin, with over 100,000 active installs, is vulnerable to an unauthenticated SQL injection (CVE-2024-43917).

The post Unpatched SQL Injection Vulnerability in TI WooCommerce Wishlist Plugin appeared first on Patchstack.

]]>

This blog post is about an unauthenticated SQL injection vulnerability on the TI WooCommerce Wishlist plugin. If you're a TI WooCommerce Wishlist user, deactivate and delete the plugin since there is no patched version available.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About TI WooCommerce Wishlist plugin

The plugin TI WooCommerce Wishlist, which has over 100,000 active installations, is one of the most popular free plugins for quickly setting up the wishlist functionality with WooCommerce on a WordPress site.

The security vulnerability

In the latest version (2.8.2 as of writing the article) and below, the plugin is vulnerable to a SQL injection vulnerability that allows any users to execute arbitrary SQL queries in the database of the WordPress site. No privileges are required to exploit the issue. The vulnerability is unpatched on the latest version and is tracked as the CVE-2024-43917.

Unauthenticated SQL injection: Case One

The underlying vulnerable code exists in the get() function:

function get( $data = array(), $count = false ) {
	global $wpdb;

	$default = array(
		'count'    => 10,
		'field'    => null,
		'offset'   => 0,
		'order'    => 'DESC',
		'order_by' => 'date',
		'external' => true,
		'sql'      => '',
	);

	foreach ( $default as $_k => $_v ) {
		if ( array_key_exists( $_k, $data ) ) {
			$default[ $_k ] = $data[ $_k ];
			unset( $data[ $_k ] );
		}
	}
	// TRIMMED
	$default['offset'] = absint( $default['offset'] );
	$default['count']  = absint( $default['count'] );
	if ( is_array( $default['field'] ) ) {
		$default['field'] = '`' . implode( '`,`', $default['field'] ) . '`';
	} elseif ( is_string( $default['field'] ) ) {
		$default['field'] = array( 'ID', $default['field'] );
		$default['field'] = '`' . implode( '`,`', $default['field'] ) . '`';
	} else {
		$default['field'] = '*';
	}
	if ( $count ) {
		$default['field'] = 'COUNT(`ID`) as `count`';
	}
	// TRIMMED
	$sql   = "SELECT {$default[ 'field' ]} FROM `{$this->table}`";
	$where = '1';
	if ( ! empty( $data ) && is_array( $data ) ) {
		if ( array_key_exists( 'meta', $data ) ) {
			unset( $data['meta'] );
		}
		foreach ( $data as $f => $v ) {
			$s = is_array( $v ) ? ' IN ' : '=';
			if ( is_array( $v ) ) {
				foreach ( $v as $_f => $_v ) {
					$v[ $_f ] = $wpdb->prepare( '%s', $_v );
				}
				$v = implode( ',', $v );
				$v = "($v)";
			} else {
				$v = $wpdb->prepare( '%s', $v );
			}
			$data[ $f ] = sprintf( '`%s`%s%s', $f, $s, $v );
		}
		$where = implode( ' AND ', $data );
		$sql   .= ' WHERE ' . $where;
	}
	// TRIMMED
	$sql .= sprintf( ' ORDER BY `%s` %s LIMIT %d,%d;', $default['order_by'], $default['order'], $default['offset'], $default['count'] ); //  [1]

	$products = $wpdb->get_results( $sql, ARRAY_A ); // WPCS: db call ok; no-cache ok; unprepared SQL ok.  [2]
	// TRIMMED
	if ( empty( $products ) || is_wp_error( $products ) ) {
		return array();
		$products[ $k ] = apply_filters( 'tinvwl_wishlist_product_get', $product );
	}

	$_products = wc_get_products( $args );
	// Filter wishlist products
	$products = apply_filters( 'tinvwl_wishlist_get_products', $products, $this );


	return $products;
}

The above function is using string concatenation to form up a SQL query with the user-input as seen in the $sql variable [1]. The $sql variable is then getting passed to $wpdb->get_results() parameter directly [2]. Tracing this function upwards to the source where we can inject the malicious SQL queries, we found that the above function is getting called by the wishlist_get_products() function:

	public function wishlist_get_products( WP_REST_Request $request ) {
		$wishlist = $this->get_wishlist_by_share_key( $request );

		if ( is_wp_error( $wishlist ) ) {
			return $wishlist;
		}

		$wlp  = new TInvWL_Product();
		$args = [
			'wishlist_id' => $wishlist['wishlist']['ID'],
			'external'    => false
		];

		if ( null !== ( $count = $request->get_param( 'count' ) ) ) {
			$args['count'] = $count;
		}
		if ( null !== ( $offset = $request->get_param( 'offset' ) ) ) {
			$args['offset'] = $offset;
		}
		if ( null !== ( $order = $request->get_param( 'order' ) ) ) { // [3]
			$args['order'] = $order;
		}


		$products = $wlp->get( $args ); // [4]

		$response = array_map( function ( $product ) use ( $request ) {
			return $this->prepare_product_data( $product, 'get_products', $request->get_params() );
		}, $products );

		return rest_ensure_response( apply_filters( 'tinvwl_api_wishlist_get_products_response', $response ) );
	}

In [3], we can see that the order parameter is getting taken from the user through $request->get_param( 'order' ). Then, the vulnerable function is getting called at [4] with the user-input. If we go back to the above get() function, this value is being directly concatenated and executed in the SQL statement confirming the SQLi. Tracing the wishlist_get_products() function itself, we found that it is called by a REST API endpoint where one can inject their malicious SQL injection queries.

register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<share_key>[A-Fa-f0-9]{6})/get_products', [
	'methods'             => WP_REST_Server::READABLE,
	'callback'            => [ $this, 'wishlist_get_products' ],
	'permission_callback' => '__return_true',
] );

In short, the whole flow of the request towards the vulnerability looks like:

REST API -> wishlist_get_products() -> get()

Unauthenticated SQL injection: Case Two

The underlying vulnerable code exists in the get_wishlists_data() function:

function get_wishlists_data( $share_key ) {

global $wpdb;

$table              = sprintf( '%s%s', $wpdb->prefix, 'tinvwl_items' );
$table_lists        = sprintf( '%s%s', $wpdb->prefix, 'tinvwl_lists' );
$table_stats        = sprintf( '%s%s', $wpdb->prefix, 'tinvwl_analytics' );
$table_translations = sprintf( '%s%s', $wpdb->prefix, 'icl_translations' );
$table_languages    = sprintf( '%s%s', $wpdb->prefix, 'icl_languages' );
$lang               = filter_input( INPUT_POST, 'lang', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); // [1]
$lang_default       = filter_input( INPUT_POST, 'lang_default', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
$stats              = filter_input( INPUT_POST, 'stats', FILTER_SANITIZE_FULL_SPECIAL_CHARS );

	$sql = "SELECT {$default[ 'field' ]} FROM `{$table}` INNER JOIN `{$table_lists}` ON `{$table}`.`wishlist_id` = `{$table_lists}`.`ID` AND `{$table_lists}`.`type` = 'default'";

	if ( $lang ) {
		if ( $lang_default ) {
			$languages = sprintf( "'%s'", implode( "', '", array( $lang, $lang_default ) ) );
		} else {
			$languages = "'" . $lang . "'";
		}

		$sql .= "LEFT JOIN {$table_translations} tr ON
{$table}.product_id = tr.element_id AND tr.element_type = 'post_product'
LEFT JOIN {$table_translations} tr2 ON
{$table}.variation_id != 0 AND {$table}.variation_id = tr2.element_id AND tr2.element_type = 'post_product_variation'
LEFT JOIN {$table_translations} t ON
tr.trid = t.trid AND t.element_type = 'post_product' AND t.language_code IN ({$languages})
LEFT JOIN {$table_translations} t2 ON
{$table}.variation_id != 0 AND tr2.trid = t2.trid AND t2.element_type = 'post_product_variation' AND t2.language_code IN ({$languages})
JOIN {$table_languages} l ON
(
t.language_code = l.code OR t2.language_code = l.code
) AND l.active = 1";
	}

	$results = $wpdb->get_results( $sql, ARRAY_A );

}

The lang and lang_default parameters [1] are being taken from the POST request and passed into the SQL query using concatenation making it vulnerable to SQLi. Tracing this back to the source, it is called in the ajax_action() function.

public function ajax_action(): void {

// TRIMMED
	if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $post['tinvwl-security'] ) && wp_verify_nonce( $post['tinvwl-security'], 'wp_rest' ) ) {
		$this->wishlist_ajax_actions( $wishlist, $post, $guest_wishlist );
	} else {
		$response = [
			'status' => false,
			'msg'    => [ __( 'Something went wrong', 'ti-woocommerce-wishlist' ) ],
			'icon'   => 'icon_big_times',
		];

		$response['msg'] = array_unique( $response['msg'] );
		$response['msg'] = implode( '<br>', $response['msg'] );

// TRIMMED
		wp_send_json( $response );
	}
}

Tracing that function back yet again, it is hooked in the wc_ajax hook.

private function define_hooks(): void {
	add_action( 'wc_ajax_tinvwl', [ $this, 'ajax_action' ] );
}

In short, the whole flow of the request towards the vulnerability looks like:

wc_ajax_tinvwl -> ajax_action() -> get_wishlists_data()

The patch

As of writing this article, there is no patched version for the plugin. If the vulnerability gets patched in the near future, we will update the article with the patch information and patched version.

Conclusion

For the SQL query process, always do a safe escape and format for the user’s input before performing a query. The best practice is to use a prepared statement and also cast each of the used variables to its intended usage. For instance, it is always better to cast a variable to an integer if the intended value of the variable should be an integer value.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

18 July, 2024We found the vulnerability and notified the vendor.
22 August, 2024Published the vulnerabilities to the Patchstack vulnerability database (No reply from vendor).
September 12, 2024Plugin closed by the WP plugin review team
25 September, 2024Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Unpatched SQL Injection Vulnerability in TI WooCommerce Wishlist Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/unpatched-sql-injection-vulnerability-in-ti-woocommerce-wishlist-plugin/feed/ 0
SQL Injection Vulnerabilities Found in ListingPro Theme and Plugin https://patchstack.com/articles/sql-injection-vulnerabilities-found-in-listingpro-theme-and-plugin/ https://patchstack.com/articles/sql-injection-vulnerabilities-found-in-listingpro-theme-and-plugin/#respond Thu, 12 Sep 2024 15:24:50 +0000 https://patchstack.com/?p=18872 This blog post is about ListingPro theme vulnerabilities. If you're a ListingPro user, please update the theme and plugin to version 2.9.5 or higher. About the ListingPro Theme and Plugin The theme ListingPro (premium version), which has over 30,000 sales, is one of the more popular premium plugins specifically related to directory and listing features. […]

The post SQL Injection Vulnerabilities Found in ListingPro Theme and Plugin appeared first on Patchstack.

]]>

This blog post is about ListingPro theme vulnerabilities. If you're a ListingPro user, please update the theme and plugin to version 2.9.5 or higher.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the ListingPro Theme and Plugin

The theme ListingPro (premium version), which has over 30,000 sales, is one of the more popular premium plugins specifically related to directory and listing features. This theme is also packed with a required plugin also named ListingPro. This plugin is developed by CridioStudio.

This theme is designed for directory and listing websites of any type. This directory WordPress theme is an all-in-one solution to running successful directory businesses as it includes all the necessary tools and plugins needed.

The security vulnerability

This theme and plugin suffers from multiple SQL Injection vulnerabilities. The theme itself is affected by Unauthenticated SQL Injection while the plugin is affected by Authenticated and Unauthenticated SQL Injection. The SQL Injection vulnerability itself allows any unauthenticated and also authenticated user to inject a malicious SQL query into a WordPress database query execution. The described vulnerabilities are patched in version 2.9.5 and assigned CVE-2024-39622, CVE-2024-39620, and CVE-2024-38795 respectively.

ListingPro Theme: Unauthenticated SQL Injection

The underlying vulnerable code exists in the generate_wire_invoice function:

function generate_wire_invoice($postid){

    global $listingpro_options, $wpdb;
    $output = null;
    $logo = '';
    $company = '';
    $address = '';
    $phone = '';
    $additional = '';
    $thanku_text = '';
    $user_name = '';
    $taxIsOn = $listingpro_options['lp_tax_swtich'];
    $tax = '';

    $logo = $listingpro_options['invoice_logo']['url'];
    $company = $listingpro_options['invoice_company_name'];
    $address = $listingpro_options['invoice_address'];
    $phone = $listingpro_options['invoice_phone'];
    $additional = $listingpro_options['invoice_additional_info'];
    $thanku_text = $listingpro_options['invoice_thankyou'];
    $userrow = '';
    $userID = '';
    $dbprefix = $wpdb->prefix;
    $counter = 1;
    $userID = '';
    $price = '';
    $invoiceno = '';
    $table = "listing_orders";
    $table =$dbprefix.$table;
    $results = array();
    if($wpdb->get_var("SHOW TABLES LIKE '$table'") == $table) {
        $query = "";
        $query = "SELECT * from $table WHERE post_id='$postid' ORDER BY main_id DESC";
        $results = $wpdb->get_results( $query);
        $results = array_reverse($results);
    }
---------------- CUT HERE ---------------- 

This function can be called from the listingpro_shortcode_checkout function listed both on include/plugins/listingpro-plugin/elementor-widgets/checkout.php and include/plugins/listingpro-plugin/shortcodes/checkout.php:

---------------- CUT HERE ---------------- 
        else if( isset($_GET['method']) && !empty($_GET['method']) && $_GET['method']=="wire" ){
            if (!isset($_SESSION)) { session_start(); }
            do_action('lp_pdf_enqueue_scripts');
            $postID = $_SESSION['post_id'];
            $discount = $_SESSION['discount'];
            if(!empty($postID)){
                $output ='<div class="page-container-four clearfix">';
                $output .='<div class="col-md-10 col-md-offset-1">';
                $output .= generate_wire_invoice( $postID);
                $output .='</div>';
                $output .='</div>';
                unset($_SESSION['post_id']);
            }
            else{
                $redirect = site_url();
                wp_redirect($redirect);
                exit();
            }
        }
---------------- CUT HERE ---------------- 
---------------- CUT HERE ---------------- 
        else if (isset($_GET['method']) && !empty($_GET['method']) && $_GET['method'] == "wire") {
            if (!isset($_SESSION)) {
                session_start();
            }
            do_action('lp_pdf_enqueue_scripts');
            $postID = $_SESSION['post_id'];
            $discount = $_SESSION['discount'];
            if (!empty($postID)) {
                $output = '<div class="page-container-four clearfix">';
                $output .= '<div class="col-md-10 col-md-offset-1">';
                $output .= generate_wire_invoice($postID);
                $output .= '</div>';
                $output .= '</div>';
                unset($_SESSION['post_id']);
            } else {
                $redirect = site_url();
                wp_redirect($redirect);
                exit();
            }
        }
---------------- CUT HERE ---------------- 

The function in both files is simply handling a shortcode and Elementor widget element. We can see that it passes $postID value to the generate_wire_invoice function and the value is coming from $_SESSION['post_id']. We can set the value via the lp_form_handler function:

function lp_form_handler($__POST, $__GET){

    session_start();

---------------- CUT HERE ---------------- 

    $method = $__POST['method'];
    $post_id = $__POST['post_id'];

---------------- CUT HERE ---------------- 

    if( !empty($method) && $method=="wire" ){
            //updating payment method
            $date = date(get_option('date_format'));
            $update_data = array('status' => 'pending', 'date' => $date, 'price' => $plan_price, 'tax' => $plan_taxPrice);
            if(!empty($new_plan_id)){
                $where = array('post_id' => $post_id, 'order_id' => $ord_num);
            }else{
                $where = array('post_id' => $post_id);
            }

            $update_format = array('%s', '%s', '%s', '%s');
            $wpdb->update($dbprefix.'listing_orders', $update_data, $where, $update_format);

            $_SESSION['post_id'] = $post_id;
---------------- CUT HERE ---------------- 

Back to the generate_wire_invoice function, since there is no proper escaping process on the $postid variable, users are able to perform SQL Injection.

ListingPro Plugin: Subscriber+ SQL Injection

The underlying vulnerable code exists on the lp_get_admin_invoice_details function:

add_action('wp_ajax_lp_get_admin_invoice_details', 'lp_get_admin_invoice_details');
if (!function_exists('lp_get_admin_invoice_details')) {
    function lp_get_admin_invoice_details()
    {
        global $wpdb;
        $invoiceid   =   $_POST['invoiceid'];
        $invoicetype   =   $_POST['invoicetype'];
        $tableName = 'listing_orders';
        if ($invoicetype == "listing") {

            $dbprefix = $wpdb->prefix;
            $myInvoice = $wpdb->get_row("SELECT * FROM " . $dbprefix . $tableName . " WHERE main_id = $invoiceid");

---------------- CUT HERE ---------------- 

        } elseif ($invoicetype == 'ads') {
            $output = null;
            $tableName = 'listing_campaigns';

            $dbprefix = $wpdb->prefix;
            $myInvoice = $wpdb->get_row("SELECT * FROM " . $dbprefix . $tableName . " WHERE main_id = $invoiceid");

---------------- CUT HERE ---------------- 

The above function is hooked to the wp_ajax_lp_get_admin_invoice_details action without any permission and nonce check. There is also no proper escaping on the $invoiceid variable which can make any authenticated users able to perform SQL Injection.

ListingPro Plugin: Unauthenticated SQL Injection

The underlying vulnerable code exists on the listingpro_save_stripe function:

add_action('wp_ajax_listingpro_save_stripe', 'listingpro_save_stripe');
add_action('wp_ajax_nopriv_listingpro_save_stripe', 'listingpro_save_stripe');

if (!function_exists('listingpro_save_stripe')) {
    function listingpro_save_stripe() {
        include_once (WP_PLUGIN_DIR ."/listingpro-plugin/inc/stripe/stripe-php/init.php");
        global $wpdb, $listingpro_options, $wp_rewrite;
        $lpURLChar = '?';
        if ($wp_rewrite->permalink_structure == '') {
            $lpURLChar = '&';
        }

---------------- CUT HERE ---------------- 

        if (isset($_POST['taxrate']) && !empty($_POST['taxrate'])) {
            $taxrate = $_POST['taxrate'];
        }
        $listing = $_POST['listing'];
        $token = $_POST['token'];
        $subsrID = '';

---------------- CUT HERE ---------------- 

        if ($charge['amount_refunded'] == 0 && $charge['failure_code'] == null && $charge['captured'] == true) {

---------------- CUT HERE ---------------- 

            $thepostt = $wpdb->get_results("SELECT * FROM " . $dbprefix . "listing_orders WHERE post_id = $listing");

---------------- CUT HERE ---------------- 

The above function is hooked to the wp_ajax_nopriv_listingpro_save_stripe action. There is also no proper escaping on the $listing variable which can make any unauthenticated users able to perform SQL Injection.

The patch

The developer decided to use a proper prepared statement and integer cast to the affected variables to prevent SQL Injection. The patch can be seen in the below diff image:

Conclusion

For the SQL query process, always do a safe escape and format for the user's input before performing a query. The best practice is always to use a prepared statement and also cast each of the used variables to its intended usage, for example, always cast a variable to an integer if the intended value of the variable should be an integer value.

Want to learn more about finding and fixing vulnerabilities?

Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.

Timeline

31 March, 2024We found the vulnerabilities and started to create a report.
1 April, 2024We reach out to the vendor regarding the vulnerabilities.
22 July, 2024Published the vulnerabilities to the Patchstack vulnerability database (No reply from vendor).
30 August, 2024ListingPro theme and plugin version 2.9.5 released to patch the reported issues.
11 September, 2024Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post SQL Injection Vulnerabilities Found in ListingPro Theme and Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/sql-injection-vulnerabilities-found-in-listingpro-theme-and-plugin/feed/ 0
Multiple Vulnerabilities in WooCommerce Amazon Affiliates Plugin https://patchstack.com/articles/multiple-vulnerabilities-in-woocommerce-amazon-affiliates-plugin/ https://patchstack.com/articles/multiple-vulnerabilities-in-woocommerce-amazon-affiliates-plugin/#respond Thu, 06 Jun 2024 09:25:33 +0000 https://patchstack.com/?p=17000 This blog post is about WooCommerce Amazon Affiliates (WZone) plugin vulnerabilities. If you're a WooCommerce Amazon Affiliates (WZone) user, please deactivate and delete the plugin since there is still no known patched version. About the WZone Plugin The plugin WZone (premium version), which has over 35,000 sales, is one of the more popular premium plugins […]

The post Multiple Vulnerabilities in WooCommerce Amazon Affiliates Plugin appeared first on Patchstack.

]]>

This blog post is about WooCommerce Amazon Affiliates (WZone) plugin vulnerabilities. If you're a WooCommerce Amazon Affiliates (WZone) user, please deactivate and delete the plugin since there is still no known patched version.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the WZone Plugin

The plugin WZone (premium version), which has over 35,000 sales, is one of the more popular premium plugins specifically related to affiliate integration between AWS and WooCommerce sites. This plugin is developed by AA-Team.

This is a premium WordPress plugin designed to help site owners and bloggers monetize their websites and make money online using the Amazon affiliate program.

The security vulnerability

This plugin suffers from multiple vulnerabilities. All of the vulnerabilities were tested on version 14.0.10 of the plugin and there is still no known patched version. These vulnerabilities also could possibly be impacting versions >= 14.0.20 of the plugin.

The first vulnerability is an Authenticated Arbitrary Option Update. This vulnerability allows any authenticated user to update arbitrary WP Options and in the worst case lead to a privilege escalation. The described vulnerability is still not patched and assigned CVE-2024-33549.

The second and third vulnerabilities are Unauthenticated and Authenticated SQL Injection. This vulnerability allows any unauthenticated and also authenticated user to inject a malicious SQL query into a WordPress database query execution. The described vulnerabilities are still not patched and assigned CVE-2024-33544 and CVE-2024-33546 respectively.

Authenticated Arbitrary Option Update

The underlying vulnerable code exists in the install_default_options function:

public function install_default_options ()
{
	unset($_REQUEST['action']);

	$is_makeinstall = isset($_REQUEST['is_makeinstall']) ? (int) $_REQUEST['is_makeinstall'] : 0;

	$serializedData = urldecode($_REQUEST['options']);

	$savingOptionsArr = array();
	parse_str($serializedData, $savingOptionsArr);

	if ( $savingOptionsArr['box_id'] == 'WooZone_setup_box' ) {
		$serializedData = preg_replace('/box_id=WooZone_setup_box&box_nonce=[\w]*&install_box=/', '', $serializedData);
		$savingOptionsArr['install_box'] = $serializedData;
		$savingOptionsArr['install_box'] = str_replace( "\\'", "\\\\'", $savingOptionsArr['install_box']);
	}

	$save_id = $savingOptionsArr['box_id'];
	unset($savingOptionsArr['box_id']);

	if( ! wp_verify_nonce( $savingOptionsArr['box_nonce'], $save_id . '-nonce')) die ('Busted!');
	unset($savingOptionsArr['box_nonce']);

	require_once( $this->cfg['paths']['plugin_dir_path'] . 'modules/setup_backup/default-sql.php');

	$savingOptionsArr['install_box'] = str_replace( '\"', '"', $savingOptionsArr['install_box']);

	$savingOptionsArr['install_box'] = str_replace('#!#', '&', $savingOptionsArr['install_box']);
	$savingOptionsArr['install_box'] = str_replace("'", "\'", $savingOptionsArr['install_box']);
	$pullOutArray = json_decode( $savingOptionsArr['install_box'], true );
	if(count($pullOutArray) == 0){
		die(json_encode( array(
			'status' => 'error',
			'html'   => "Invalid install default json string, can't parse it!"
		)));
	}else{

		foreach ($pullOutArray as $key => $value){
			if  ( $is_makeinstall && get_option( $key, false ) ) {
				continue 1;
			}

			update_option( $key, $saveIntoDb );
		}

		update_option( $this->alias . "_is_installed", 'true');

		die(json_encode( array(
			'status' => 'ok',
			'html'   => 'Install default successful'
		)));
	}
}

This function is hooked under the wp_ajax_WooZoneInstallDefaultOptions action and doesn't have a proper permission check. It does have a nonce check, however, the nonce value can be fetched from any authenticated (Subscriber+) users. The function will store $_REQUEST['options'] value inside of $serializedData variable and then it will parse the value to $savingOptionsArr. The function then constructs a $pullOutArray variable from $savingOptionsArr and will do a loop on each of the values to perform an update_option function call.

Unauthenticated SQL Injection

The underlying vulnerable code exists on the product_by_asin function:

public function product_by_asin( $asins=array() ) {
	$asins = array_unique( array_filter($asins) );
	if (empty($asins)) return array();

	$key = '_amzaff_prodid';
	$_key = '_amzASIN';

	$return = array_fill_keys( $asins, false );

	global $wpdb;

	$asins_ = implode(',', array_map(array($this, 'prepareForInList'), $asins));

	$sql_asin2id = "select pm.meta_value as asin, p.* from " . $wpdb->prefix.'posts' . " as p left join " . $wpdb->prefix.'postmeta' . " as pm on p.ID = pm.post_id where 1=1 and !isnull(p.ID) and pm.meta_key = '$key' and pm.meta_value != '' and pm.meta_value in ($asins_);";
	$res_asin2id = $wpdb->get_results( $sql_asin2id, OBJECT_K );
	//var_dump('<pre>', $res_asin2id , '</pre>'); echo __FILE__ . ":" . __LINE__;die . PHP_EOL;
	if ( !empty($res_asin2id) ) {
		foreach ($res_asin2id as $k => $v) {
			$asin = $v->asin;
			$return["$asin"] = $v;
		}
	}

	// because we have old amazon products which have only '_amzASIN' meta (they don't have this new '_amzaff_prodid' meta)
	$sql_asin2id = "select pm.meta_value as asin, p.* from " . $wpdb->prefix.'posts' . " as p left join " . $wpdb->prefix.'postmeta' . " as pm on p.ID = pm.post_id where 1=1 and !isnull(p.ID) and pm.meta_key = '$_key' and pm.meta_value != '' and pm.meta_value in ($asins_);";
	$res_asin2id = $wpdb->get_results( $sql_asin2id, OBJECT_K );
--------------- CUT HERE ---------------

The above function can be called from the WooZone_product_by_asin function:

if ( !function_exists('WooZone_product_by_asin') ) {
	function WooZone_product_by_asin( $asins=array() ) {
		global $WooZone;
		return $WooZone->product_by_asin( $asins );
	}
}

The above function can be called from the add_product function that has the purpose of handling the product addition process:

public function add_product( $product, $pms=array() ) {
	$pms = array_replace_recursive( array(
		// !!! true - only when you know what you're doing on this code
		'debug' 		=> false,

		'where_from' 	=> 'chrome-extension', // chrome-extension | module-noawskeys

		// (integer) number of images per variation child for additional variation images woozone plugin
		'avi_nbvars' 	=> 1,

		// (integer from 0) 0 = use category from amazon (use browse nodes to build a category structure like on amazon)
		'idcateg' 		=> 0,

		// (integer from 1 or string 'all')
		'nbimages' 		=> 'all',

		// (integer from 0 or string 'all')
		'nbvariations' 	=> 'all',

		// (integer 0 | 1)
		'spin' 			=> 0,

		// (integer 0 | 1)
		'attributes' 	=> 1,
	), $pms);
	extract( $pms );

	$ret = array(
		'status' => 'invalid',
		'msg' => '',
		'msg_arr' => array(),
		'msg_full' => '',
		'msg_summary' => '',
		'product_id' => 0,
		'duration' => 0,
	);

	if ( $avi_nbvars < 1 || $avi_nbvars > $this->the_plugin->ss['max_images_per_variation'] ) {
		$avi_nbvars = 1;
		$pms['avi_nbvars'] = $avi_nbvars;
	}
	$this->avi_nbvars = $avi_nbvars;


	if ( 'php://input' === $product ) {
		$product = $this->the_plugin->wp_filesystem->get_contents( 'php://input' );
		if ( ! $product ) {
			$product = file_get_contents( 'php://input' );
		}
		$product = json_decode( $product, true );
	}

	if ( $debug ) {
		//require_once( '_test/product.inc.php' );
		$product = $this->the_plugin->cfg['paths']['scripts_dir_path'] . '/directimport/_test/B0769XD5YC.json';
		$product = file_get_contents( $product );
		$product = json_decode( $product, true );
	}

	//die( var_dump( "<pre>", $product  , "<pre>" ) . PHP_EOL .  __FILE__ . ":" . __LINE__  );


	//:: verify product has an asin?
	$opValidProduct = $this->is_valid_product_asin( $product );
	if ( ! $opValidProduct ) {
		$ret = array_replace_recursive($ret, array(
			'msg' => 'Product ASIN is missing!',
		));
		$ret['msg_summary'] = $ret['msg'];
		return $ret;
	}
	$asin = $product['ASIN'];


	//:: verify if product already is imported?
	$opAsinExist = WooZone_product_by_asin( array($asin) );
--------------- CUT HERE ---------------

The above function itself can be called from the _product_import function:

private function _product_import( $product, $pms=array() ) {
	$pms = array_replace_recursive( array(
		'where_from' 	=> 'module-noawskeys',
		'avi_nbvars' 	=> 1,
		'idcateg' 		=> 0,
		'nbimages' 		=> 'all',
		'nbvariations' 	=> 5,
		'spin' 			=> 0,
		'attributes' 	=> 1,
	), $pms);

	$ret = array(
		'status' => 'invalid',
		'msg' => '',
		'msg_arr' => array(),
		'msg_full' => '',
		'msg_summary' => '',
		'product_id' => 0,
		'duration' => 0,
	);

	$opStatus = WooZoneDirectImport()->add_product( $product, $pms );
	$ret['duration_import'] = $ret['duration'];
	unset( $ret['duration'] );
	return array_replace_recursive( $ret, $opStatus );
}

The above function acts as a function handler to import product data. When traced back, the function actually can be called from the ajax_request function that is hooked to the wp_ajax_nopriv_WooZoneNoAWSImport action.

public function ajax_request() {  
	$requestData = array(
		'action' 		=> isset($_REQUEST['sub_action']) ? (string) $_REQUEST['sub_action'] : '',
		'debug_step' 	=> isset($_REQUEST['debug_step']) ? (string) $_REQUEST['debug_step'] : '',
	);
	extract($requestData);
	//var_dump('<pre>', $requestData , '</pre>'); echo __FILE__ . ":" . __LINE__;die . PHP_EOL;

	$ret = array(
		'status' => 'invalid',
		'msg' => 'Invalid action!',
	);

	if ( empty($action) || !in_array($action, array(
		'add_product',
	)) ) {
		die(json_encode($ret));
	}

	//:: actions
	switch ( $action ) {

		case 'add_product':
			$duration = array();

			// extract product data
			$timer_start = $this->timer_start();
			if( isset($_REQUEST['from']) && $_REQUEST['from'] == 'extension_amz_page' ){
				$request = $this->the_plugin->wp_filesystem->get_contents( 'php://input' );
				if ( ! $product ) {
					$request = file_get_contents( 'php://input' );
				}
				$request = json_decode( $request, true );

				$_REQUEST = array_merge($_REQUEST, $request);
				
				// chrome.runtime.sendMessage dont' add slashes
				$_REQUEST['variations'] = addslashes($_REQUEST['variations']);
			}
				
			$pmsGetProduct = array(
				'debug_step' 	=> $debug_step,
				'page_content' 	=> isset($_REQUEST['page_content']) ? (string) $_REQUEST['page_content'] : null,
				'asin' 			=> isset($_REQUEST['asin']) ? (string) $_REQUEST['asin'] : null,
				'country' 		=> isset($_REQUEST['country']) ? (string) $_REQUEST['country'] : null,
				'variations' 	=> isset($_REQUEST['variations']) ? (string) $_REQUEST['variations'] : null,
			);
			
			//die( var_dump( "<pre>", $pmsGetProduct  , "<pre>" ) . PHP_EOL .  __FILE__ . ":" . __LINE__  ); 
			
			
			if ( 'get_params' === $debug_step ) {
				//var_dump('<pre>', $pmsGetProduct , '</pre>'); echo __FILE__ . ":" . __LINE__;die . PHP_EOL;
			}

			$opGetProduct = $this->_product_extract_data( $pmsGetProduct );
			//die( var_dump( "<pre>", $opGetProduct  , "<pre>" ) . PHP_EOL .  __FILE__ . ":" . __LINE__  ); 
			if ( 'extract_data' === $debug_step ) {
				//var_dump('<pre>', $opGetProduct , '</pre>'); echo __FILE__ . ":" . __LINE__;die . PHP_EOL;
			}
			$ret = array_replace_recursive( $ret, $opGetProduct, array( 'msg_full' => '', 'msg_summary' => '', 'msg_arr' => array() ) );
			//die( json_encode( $ret ) ); //DEBUG
			unset( $ret['data'] );

			$timer_end = $this->timer_end();
			$duration["_product_extract_data"] = $timer_end;

			if ( 'invalid' === $opGetProduct['status'] ) {
				break;
			}

			// import product
			$timer_start = $this->timer_start();

			$pmsImportProduct = array(
				//'avi_nbvars' 	=> isset($_REQUEST['avi_nbvars']) ? (int) $_REQUEST['avi_nbvars'] : 1,
				'idcateg' 		=> isset($_REQUEST['idcateg']) ? (int) $_REQUEST['idcateg'] : 0,
				'nbimages' 		=> isset($_REQUEST['nbimages']) ? (string) $_REQUEST['nbimages'] : 'all',
				'nbvariations' 	=> isset($_REQUEST['nbvariations']) ? (string) $_REQUEST['nbvariations'] : 5,
				'spin' 			=> isset($_REQUEST['spin']) ? (int) $_REQUEST['spin'] : 0,
				'attributes' 	=> isset($_REQUEST['attributes']) ? (int) $_REQUEST['attributes'] : 1,
			);
			//var_dump('<pre>', $pmsImportProduct , '</pre>'); echo __FILE__ . ":" . __LINE__;die . PHP_EOL;
			//var_dump('<pre>', $opGetProduct['data'] , '</pre>'); echo __FILE__ . ":" . __LINE__;die . PHP_EOL;
			$opImportProduct = $this->_product_import( $opGetProduct['data'], $pmsImportProduct );
--------------- CUT HERE ---------------

Notice on the above function, that there is no proper permission and nonce check, meaning that any unauthenticated users are able to trigger the function and perform product import. Let's analyze the function.

First, users are able to construct $pmsGetProduct variable with arbitrary supplied input on several keys, including the "asin" key. The _product_extract_data function is just simply a function to prepare for the import data process and will put the initial value including the "asin" key value inside of the $opGetProduct['data'] object.

Back to the product_by_asin function, from all of the above conditions mentioned, users are able to fully control the $asins value. Before the SQL query process, the function will construct a $asins_ from $asins variable using the prepareForInList function:

public function prepareForInList($v) {
	return "'".$v."'";
}

So, our input will be wrapped in single quotes. If you are already familiar with WordPress, you must be aware that WordPress will apply magic quotes to PHP global input, so if users directly supply the SQL Injection payload via the $_REQUEST variable, any quotes will be escaped and SQL Injection is not possible in this case. However, let's look back to the ajax_request function, and notice that the function will construct a $request variable via the file_get_contents( 'php://input' ) and it will treat it as a JSON object. Then, the function will merge the $request variable to the $_REQUEST global variable, so in this case, users are able to inject the SQL Injection payload by using JSON as the input content type to bypass the magic quotes.

Authenticated SQL Injection

The underlying vulnerable code exists on the action_edit_inline function:

public function action_edit_inline()
{
	global $wpdb;

	$ret = array(
		'status'        => 'invalid',
		'msg'          => '',
	);
	
	$request = array(
		'table'			=> isset($_REQUEST['table']) ? trim((string)$_REQUEST['table']) : '',
		'itemid' 		=> isset($_REQUEST['itemid']) ? (int)$_REQUEST['itemid'] : 0,
		'field_name'	=> isset($_REQUEST['field_name']) ? trim((string)$_REQUEST['field_name']) : '',
		'field_value'	=> isset($_REQUEST['field_value']) ? trim((string)$_REQUEST['field_value']) : '',
	);
	extract($request);
	
	$status = 'invalid'; $status_msg = '';
	if( $request['itemid'] > 0 ) {
		$table = $wpdb->prefix  . $table;
		if ( 1 ) {
			// update field
			if ( 1 ) {
				$wpdb->update(
					$table, 
					array( 
						$field_name		=> $field_value
					), 
					array( 'id' => $itemid ), 
					array( 
						'%s'
					), 
					array( '%d' ) 
				);
			}
--------------- CUT HERE ---------------

The above function can be called from the ajax_request function:

public function ajax_request( $retType='die', $pms=array() ) {
	$request = array(
		'action'             => isset($_REQUEST['sub_action']) ? $_REQUEST['sub_action'] : '',
		'ajax_id'            => isset($_REQUEST['ajax_id']) ? $_REQUEST['ajax_id'] : '',
	);
	extract($request);
	//var_dump('<pre>', $request, '</pre>'); die('debug...');

	$ret = array(
		'status'        => 'invalid',
		'html'          => '',
	);
	
	if ( in_array($action, array('publish', 'delete', 'bulk_delete')) ) {
		// maintain box html
		$_SESSION['WooZoneListTable'][$request['ajax_id']]['requestFrom'] = 'ajax';
		$this->setup( $_SESSION['WooZoneListTable'][$request['ajax_id']] );
	}

	$opStatus = array();
	if ( 'publish' == $action ) {
		$opStatus = $this->action_publish();
	}
	else if ( 'delete' == $action ) {
		$opStatus = $this->action_delete();
	}
	else if ( 'bulk_delete' == $action ) {
		$opStatus = $this->action_bulk_delete();
	}
	else if ( 'edit_inline' == $action ) {
		$opStatus = $this->action_edit_inline();
	}
--------------- CUT HERE ---------------

The function is hooked to the wp_ajax_WooZoneAjaxList_actions action. Since there is no proper permission and nonce check, any authenticated users are able to trigger the function.

Let's go back to the action_edit_inline function, the function itself will construct a $request variable with the following keys "table", "itemid", "field_name" and "field_value". Users in this case are able to control the value of every key of the variable object. The function then performs an extract process and then will perform $wpdb->update() using the arbitrary values that are fully controlled by the users. With this condition, users are able to modify any table data on the database.

Disclosure note

We decided to release this security advisory article since we haven't received any reply from the vendor. We also noticed that the affected plugin has an old vulnerability entry that hasn't been patched here.

The patch

There is still no known official patch for the mentioned vulnerabilities. Although there is a vPatch available for all paid Patchstack users.

Conclusion

The most important thing when implementing an action or process is to apply permission or role and nonce validation. Permission or role check could be validated using current_user_can function and nonce value could be validated using wp_verify_nonce or check_ajax_referer.

For the SQL query process, always do a safe escape and format for the user's input before performing a query, and never give arbitrary access for users to update tables on the database.

Timeline

25 February, 2024We found the vulnerability and start to create a reports.
26 February, 2024We reach out to the vendor regarding the vulnerabilities.
25 April, 2024Published the vulnerabilities to the Patchstack vulnerability database (No reply from vendor). Deployed vPatch to protect our users.
29 May, 2024Envato notified about the vulnerabilities.
06 June, 2024Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Multiple Vulnerabilities in WooCommerce Amazon Affiliates Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/multiple-vulnerabilities-in-woocommerce-amazon-affiliates-plugin/feed/ 0
Critical Vulnerabilities Found in XStore Theme and Plugin https://patchstack.com/articles/critical-vulnerabilities-found-in-xstore-theme-and-plugin/ https://patchstack.com/articles/critical-vulnerabilities-found-in-xstore-theme-and-plugin/#respond Tue, 14 May 2024 08:37:24 +0000 https://patchstack.com/?p=17219 This blog post is about the XStore theme and plugin vulnerabilities. If you're an XStore user, please update the theme to at least version 9.3.9 and the plugin to at least version 5.3.9. About the XStore Theme and Plugin The theme XStore (premium version), which has over 44,000 sales, is known as the more popular […]

The post Critical Vulnerabilities Found in XStore Theme and Plugin appeared first on Patchstack.

]]>

This blog post is about the XStore theme and plugin vulnerabilities. If you're an XStore user, please update the theme to at least version 9.3.9 and the plugin to at least version 5.3.9.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the XStore Theme and Plugin

The theme XStore (premium version), which has over 44,000 sales, is known as the more popular WooCommerce-focused premium theme in WordPress. This theme is bundled with a required XStore Core plugin. This theme is developed by 8theme.

This premium WordPress theme is a theme that helps users to build stunning online stores with WordPress and WooCommerce.  This theme provides over 130+ pre-made demos and can be used to customize brands and products.

The security vulnerability

The XStore theme itself suffers from multiple critical vulnerabilities.

The first vulnerability is Unauthenticated Local File Inclusion. This vulnerability allows any unauthenticated user to include arbitrary PHP files that are available on the server. In the worst case, this could lead to code execution if the user can fully or partially control some content on the PHP files on the server.

The second vulnerability is Unauthenticated SQL Injection. This vulnerability allows any unauthenticated user to inject a malicious SQL query into a WordPress database query execution.

The third vulnerability is Authenticated Arbitrary Option Update. This vulnerability allows any authenticated user to update arbitrary WP Options and in the worst-case lead to a privilege escalation. The described vulnerabilities were fixed in version 9.3.9 and assigned CVE-2024-33560, CVE-2024-33559, and CVE-2024-33564 respectively.

The required XStore Core plugin also suffers from multiple critical vulnerabilities.

The first vulnerability is Unauthenticated SQL Injection. This vulnerability allows any unauthenticated user to inject a malicious SQL query into a WordPress database query execution.

The second vulnerability is Unauthenticated PHP Object Injection. This vulnerability allows any unauthenticated user to pass ad-hoc serialized strings to a vulnerable unserialize call, resulting in an arbitrary PHP object(s) injection into the application scope and could result in a Remote Code Execution in a worst case.

The third vulnerability is Unauthenticated Account Takeover. This vulnerability allows any unauthenticated user to set any user's password and take over their account. The described vulnerabilities were fixed in version 5.3.9 and assigned CVE-2024-33551, CVE-2024-33552, and CVE-2024-33553 respectively.

XStore Theme: Unauthenticated Local File Inclusion

The underlying vulnerable code exists in the require_class function:

public function require_class($class=''){
    if (! $class){
        return;
    }
    require_once( apply_filters('etheme_file_url', ETHEME_CODE . 'panel/classes/'.$class.'.php') );
}

The function itself is called in many areas of the code, one of which is located on the main_construct function:

public function main_construct(){
    add_action( 'admin_menu', array( $this, 'et_add_menu_page' ) );
    add_action( 'admin_head', array( $this, 'et_add_menu_page_target') );
    add_action( 'wp_ajax_et_ajax_panel_popup', array($this, 'et_ajax_panel_popup') );

    // Enable svg support
    add_filter( 'upload_mimes', [ $this, 'add_svg_support' ] );
    add_filter( 'wp_check_filetype_and_ext', array( $this, 'correct_svg_filetype' ), 10, 5 );

    if ( isset($_REQUEST['helper']) && $_REQUEST['helper']){
        $this->require_class($_REQUEST['helper']);
    }
------- CUT HERE -------

The main_construct function itself will be called when the framework/panel/panel.php file is loaded. Below is the code that tries to load the file:

if ( is_admin() ) {
	require_once( apply_filters('etheme_file_url', ETHEME_CODE . 'system-requirements.php') );

    require_once( apply_filters('etheme_file_url', ETHEME_CODE . 'patcher.php') );

	// require_once( apply_filters('etheme_file_url', ETHEME_CODE . 'thirdparty/fonts_uploader/etheme_fonts_uploader.php') );
	
	require_once( apply_filters('etheme_file_url', ETHEME_CODE . 'admin.php') );

	require_once( apply_filters('etheme_file_url', ETHEME_CODE . 'admin/widgets/class-admin-sidebasr.php') );

	require_once( apply_filters('etheme_file_url', ETHEME_CODE . 'panel/panel.php') );
------- CUT HERE -------

With this condition, users can trigger the require_class function by simply visiting the /wp-admin/admin-ajax.php endpoint. Back to the main_construct function, it will directly pass the $_REQUEST['helper'] parameter to the require_class function. Since there is no sanitization or filtering on the $class variable inside of the require_class function, users can simply do a path traversal to include arbitrary local PHP files.

XStore Theme: Unauthenticated SQL Injection

The underlying vulnerable code exists in the framework/woo.php file:

add_filter( 'posts_clauses', function ($clauses, $query){
	global $wpdb;
	if(isset($query->query_vars['single_variations_filter']) && $query->query_vars['single_variations_filter']=='yes'){
		$_chosen_attributes = \WC_Query::get_layered_nav_chosen_attributes();
		$min_price          = isset( $_GET['min_price'] ) ? wc_clean( wp_unslash( $_GET['min_price'] ) ) : 0; // WPCS: input var ok, CSRF ok.
		$max_price          = isset( $_GET['max_price'] ) ? wc_clean( wp_unslash( $_GET['max_price'] ) ) : 0; // WPCS: input var ok, CSRF ok.
		$rating_filter      = isset( $_GET['rating_filter'] ) ? array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ) : array(); // 
------- CUT HERE -------
		if ( $min_price ) {
			$clauses['where'] .= " AND {$wpdb->posts}.ID IN (SELECT $wpdb->postmeta.post_id FROM $wpdb->postmeta WHERE meta_key = '_price' AND meta_value >= $min_price)";
		}
		if ( $max_price ) {
			$clauses['where'] .= " AND {$wpdb->posts}.ID IN (SELECT $wpdb->postmeta.post_id FROM $wpdb->postmeta WHERE meta_key = '_price' AND meta_value <= $max_price)";
		}
------- CUT HERE -------
		if ( isset($_GET['stock_status'])) {
			$stock = str_replace('_','', $_GET['stock_status']);
			$stock = explode(',',$stock);
			$stock_by = array();
			
			foreach ($stock as $stock_val) {
				$stock_by[] = "'".$stock_val."'";
			}
			$clauses['where'] .= "AND {$wpdb->posts}.ID IN (SELECT {$wpdb->postmeta}.post_id FROM {$wpdb->postmeta}
				WHERE ( meta_key = '_stock_status' AND meta_value IN (".implode(',', $stock_by).") ) ) ";
		}
------- CUT HERE -------

Notice that the code tries to add a filter of posts_clauses hook with some additional code. The hook itself allows a developer to filter all query clauses at once, for convenience. It also covers the WHERE, GROUP BY, JOIN, ORDER BY, DISTINCT, fields (SELECT), and LIMIT clauses. In the above code itself, variables such as $min_price, $max_price, and $stock_by are appended to the $clauses['where'] without proper sanitization. For $min_price and $max_price variables, using the wc_clean function alone is not enough to prevent SQL Injection.

XStore Theme: Authenticated Arbitrary Option Update

The underlying vulnerable code exists in the xstore_panel_settings_save function:

public function xstore_panel_settings_save() {
    $settings_name = isset( $_POST['settings_name'] ) ? $_POST['settings_name'] : $this->settings_name;
    $all_settings            = (array)get_option( $settings_name, array() );
    $local_settings          = isset( $_POST['settings'] ) ? $_POST['settings'] : array();
    if ( isset( $_POST['type'] ) ) {
        $local_settings_key = $_POST['type'];
    }
    else {
        switch ( $settings_name ) {
            case 'xstore_sales_booster_settings':
                $local_settings_key = 'fake_sale_popup';
                break;
            default:
                $local_settings_key = 'general';
        }
    }
    $updated                 = false;
    $local_settings_parsed   = array();

    foreach ( $local_settings as $setting ) {
//			$local_settings_parsed[ $local_settings_key ][ $setting['name'] ] = $setting['value'];
        // if ( $this->settings_name == 'xstore_sales_booster_settings' )
        $local_settings_parsed[ $local_settings_key ][ $setting['name'] ] = stripslashes( $setting['value'] );
    }

    $all_settings = array_merge( $all_settings, $local_settings_parsed );

    update_option( $settings_name, $all_settings );
------- CUT HERE -------

The function itself is registered via a wp_ajax action. Notice that there is no proper permission and nonce check on the function, resulting in any authenticated user being able to trigger the function. Also, notice that there is no filtering or whitelist check to the $settings_name value which is used on the update_option function. This could allow users to update arbitrary WP Options on the site. However, since the $all_settings value itself can't be fully controlled by the users, we are not able to achieve a worst-case impact which is a privilege escalation by enabling the user registration option and setting the default role of registration to the Administrator role.

XStore Core Plugin: Unauthenticated SQL Injection

The underlying vulnerable code exists in the get_status_count function:

public function get_status_count($type = 'outofstock', $k = '_stock_status', $q = '='){
	$filter_brand = isset( $_GET['filter_brand'] ) ? $_GET['filter_brand'] : '' ;
	$_chosen_attributes = \WC_Query::get_layered_nav_chosen_attributes();
	$min_price          = isset( $_GET['min_price'] ) ? wc_clean( wp_unslash( $_GET['min_price'] ) ) : 0; // WPCS: input var ok, CSRF ok.
	$max_price          = isset( $_GET['max_price'] ) ? wc_clean( wp_unslash( $_GET['max_price'] ) ) : 0; // WPCS: input var ok, CSRF ok.
	$rating_filter      = isset( $_GET['rating_filter'] ) ? array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ) : array(); // WPCS: sanitization ok, input var ok, CSRF ok.
	$stock_status       = isset( $_GET['stock_status'] ) ? wc_clean( wp_unslash( $_GET['stock_status'] ) ) : 0;
	$sale_status       = isset( $_GET['sale_status'] ) ? wc_clean( wp_unslash( $_GET['sale_status'] ) ) : 0;

	global $wpdb;

	// new
	if ($this->request_type == 'new'){
		$isd = array();

------- CUT HERE -------

		// merge with filters
		$isd = array_merge(
			$isd,
			$this->get_price_ids($wpdb, $min_price, $max_price), // price filter
			$this->get_rating_ids($wpdb, $rating_filter), // rating_filter
			$this->get_brand_ids($wpdb, $filter_brand), // filter_brand
			$this->get_attributes_ids($wpdb, $_chosen_attributes), // chosen_attributes
			$this->get_taxonomy_ids($wpdb) // $taxonomy
		);

------- CUT HERE -------

		if (count($isd)){
			$outofstock .= "AND p.ID IN (".implode(',', $isd).")";
		}

		return count($wpdb->get_results( $outofstock, OBJECT_K ));
	}
	else {
------- CUT HERE -------
		// $min_price
		if ($min_price){
			$outofstock .= "
			INNER JOIN $wpdb->postmeta pm3
			on p.ID = pm3.post_id
			AND pm3.meta_key='_price'
			AND pm3.meta_value >= $min_price
		";
		}

		// $max_price
		if ($max_price){
			$outofstock .= "
			INNER JOIN $wpdb->postmeta pm4
			on p.ID = pm4.post_id
			AND pm4.meta_key='_price'
			AND pm4.meta_value <= $max_price
		";
		}
------- CUT HERE -------
		return count($wpdb->get_results( $outofstock, OBJECT_K ));
	}
}

The function itself can be called from Product Status Filters custom widget and it can be triggered by an unauthenticated user. Notice that similar to the vulnerability on the XStore Theme, the $min_price and $max_price variables are not properly sanitized and it will be passed insecurely to the $outofstock variable. Additionally, the variables also passed to the get_price_ids function which is also vulnerable to SQL Injection:

private function get_price_ids($wpdb, $min_price, $max_price){
    $min_price_ids = array();
    $max_price_ids = array();
    // $min_price
    if ($min_price){
        $min_price_ids = "SELECT ID FROM $wpdb->posts as p 
            INNER JOIN $wpdb->postmeta pm
            on p.ID = pm.post_id
            and  pm.meta_key='_price'
            AND pm.meta_value  >= $min_price
            AND p.post_status = 'publish'
            AND p.post_type = 'product'
        ";
        $min_price_ids = array_keys($wpdb->get_results( $min_price_ids, OBJECT_K  ));
    }

    // $max_price
    if ($max_price){
        $max_price_ids = "SELECT ID FROM $wpdb->posts as p 
            INNER JOIN $wpdb->postmeta pm
            on p.ID = pm.post_id
            and  pm.meta_key='_price'
            AND pm.meta_value  <= $max_price
            AND p.post_status = 'publish'
            AND p.post_type = 'product'
        ";
        $max_price_ids = array_keys($wpdb->get_results( $max_price_ids, OBJECT_K  ));
    }

    return array_merge($min_price_ids, $max_price_ids);
}

XStore Core Plugin: Unauthenticated PHP Object Injection

The underlying vulnerable code exists in the process_callback function:

public function process_callback() {
	if (
		isset($_GET['error'])
		&& isset($_GET['error_description'])
		&& isset($_GET['error_reason'])
		&& isset($_GET['error_code'])
	){
		$page = ( is_checkout() ) ? 'checkout' : 'myaccount';
		wp_safe_redirect(wc_get_page_permalink($page));
		exit;
	}

	if( empty( $_GET['opauth'] ) ) return;

	$redirect = true;

	$opauth = unserialize(etheme_decoding($_GET['opauth']));
------- CUT HERE -------

The function is attached to the template_redirect hook and can be triggered by an unauthenticated user. Let's take a look at etheme_decoding function:

function etheme_decoding( $val ) {
	return base64_decode( $val );
}

With this condition, the user can simply pass a chained serialized object with base64 encoded format and trigger the PHP Object Injection. Until this advisory article was out, we weren't able to chain a necessary object to achieve RCE.

XStore Core Plugin: Unauthenticated Account Takeover

The underlying vulnerable code exists in the process_callback function:

public function process_callback() {
	if (
		isset($_GET['error'])
		&& isset($_GET['error_description'])
		&& isset($_GET['error_reason'])
		&& isset($_GET['error_code'])
	){
		$page = ( is_checkout() ) ? 'checkout' : 'myaccount';
		wp_safe_redirect(wc_get_page_permalink($page));
		exit;
	}

	if( empty( $_GET['opauth'] ) ) return;

	$redirect = true;

	$opauth = unserialize(etheme_decoding($_GET['opauth']));

	if( empty( $opauth['auth']['info'] ) ) {
		$error = sprintf(
			"%s %s %s",
			esc_html__( 'Can\'t login with.', 'xstore-core' ),
			( isset($opauth['auth']) && isset($opauth['auth']['provider']) ) ? $opauth['auth']['provider'] : 'undefined',
			esc_html__( 'Please, try again later.', 'xstore-core' )
		);
		wc_add_notice( $error, 'error' );
		return;
	}

	$info = $opauth['auth']['info'];

	if( empty( $info['email'] ) ) {
		$error = sprintf(
			"%s %s",
			$opauth['auth']['provider'],
			esc_html__( 'doesn\'t provide your email. Try to register manually.', 'xstore-core' )
		);
		wc_add_notice( $error, 'error' );
		return;
	}

	add_filter('dokan_register_nonce_check', '__return_false');
	add_filter('pre_option_woocommerce_registration_generate_username', array($this,'generate_username_option'), 10);

	$password = wp_generate_password();

	if ( ! empty( $info['first_name'] ) && ! empty( $info['last_name'] ) ) {
		$udata = array(
			'first_name' => $info['first_name'],
			'last_name' => $info['last_name']
		);
	} else {
		$udata = array();
	}

	$customer = wc_create_new_customer( $info['email'], '', $password, $udata);

	$user = get_user_by('email', $info['email']);

	$image = false;

	if (isset($info['image'])){
		$image = $info['image'];
	} elseif (isset($info['picture'])){
		$image = $info['picture'];
	}

	if (get_theme_mod( 'load_social_avatar_value', 'off' ) === 'on' && $image){
		$this->setup_avatar($user, $image);
	}

	if( is_wp_error( $customer ) ) {
		if( isset( $customer->errors['registration-error-email-exists'] ) ) {
			wc_set_customer_auth_cookie( $user->ID );
		}
	} else {
		wc_set_customer_auth_cookie( $customer );
	}

This function itself handles the process of third-party login. Notice that the function will try to call wc_set_customer_auth_cookie which will log in a customer and set the necessary authentication cookie. There is no proper check on how the $user object is built. We can simply set the $info['email'] value to any user's email to take over their account and login as that user.

The patch

XStore Theme patch

For the Unauthenticated Local Inclusion vulnerability, the vendor applies a sanitize_file_name function to prevent path traversal. The patch can be seen below:

For the Unauthenticated SQL Injection vulnerability, the vendor applies a floatval casting to the $_GET['min_price'] and $_GET['max_price'] variables as well as applying esc_sql function to the $stock_val variable. The patch can be seen below:

For the Authenticated Arbitrary Option Update vulnerability, the vendor decided to apply a nonce check (where the nonce value could only be fetched by a privileged user) and add a whitelist check to the $settings_name variable. The patch can be seen below:

XStore Core Plugin patch

For the Unauthenticated SQL Injection vulnerability, the vendor applies a floatval casting to the $_GET['min_price'] and $_GET['max_price'] variables. The patch can be seen below:

For the Unauthenticated PHP Object Injection vulnerability, the vendor decided to remove the unserialize function and replace it with the json_decode function instead. The patch can be seen below:

For the Authenticated Account Takeover vulnerability, the vendor decided to store all of the necessary user data to the et_auth-signature-* transient and use the stored value to log in the user instead of directly from user input. The patch can be seen below:

Conclusion

The vulnerabilities discussed here underscore the importance of securing all aspects of a plugin.

In the context of SQL query execution, we recommend developers to force cast the value to an integer or float if the intended value is indeed an integer or float value before constructing the value to the SQL query. We can also use $wpdb->prepare() statements by specifying "%d" or "%f" as the input format.

In the context of local file inclusion, we recommend applying a sanitization using sanitize_file_name function to prevent path traversal and additionally apply a strict whitelist check to only allow certain files to be included.

In the context of PHP object injection, we recommend entirely not to use a unserialize process and use other data formats such as JSON to store and fetch more complex data.

In the context of arbitrary option updates, we recommend applying a proper permission and nonce check to the related function or process and also applying a whitelist check on what option key could be updated.

Lastly, in the context of account takeover, especially case of third-party login, we recommend properly checking and verifying user data via the related third-party endpoint and not directly accepting data from user input for the login process.

Timeline

08 March, 2024Vulnerabilities found, reports generated.
09 March, 2024Vendor notified about vulnerabilities.
25 April, 2024No response from the vendor. Added the vulnerabilities to the Patchstack vulnerability database.
03 May, 2024Vendor replied.
04-06 May, 2024Vendor submits a proposed patch and we perform a patch validation.
07 May, 2024XStore theme version 9.3.9 alongside XStore Core plugin version 5.3.9 released to patch the reported issues.
14 May, 2024Security advisory article published.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Critical Vulnerabilities Found in XStore Theme and Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/critical-vulnerabilities-found-in-xstore-theme-and-plugin/feed/ 0
Critical Vulnerabilities Patched in REHub Theme and Plugin https://patchstack.com/articles/critical-vulnerabilities-patched-in-rehub-theme-and-plugin/ https://patchstack.com/articles/critical-vulnerabilities-patched-in-rehub-theme-and-plugin/#respond Wed, 03 Apr 2024 08:08:41 +0000 https://patchstack.com/?p=17223 This blog post is about the REHub theme and plugin vulnerabilities. If you're a REHub user, please update the plugin to at least version 19.6.2 on both the theme and the plugin. About the REHub Theme and Plugin The theme REHub (premium version), which has over 35,000 sales, is known as the more popular price […]

The post Critical Vulnerabilities Patched in REHub Theme and Plugin appeared first on Patchstack.

]]>

This blog post is about the REHub theme and plugin vulnerabilities. If you're a REHub user, please update the plugin to at least version 19.6.2 on both the theme and the plugin.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the REHub Theme and Plugin

The theme REHub (premium version), which has over 35,000 sales, is known as the more popular price comparison and multi-vendor marketplace theme in WordPress. This theme is bundled with a required REHub Framework plugin. This theme is developed by Sizam Design.

This premium WordPress theme is a modern multipurpose hybrid theme. This theme covers many modern business models for online websites. Each part can be configured and used separately, and we can combine them all in one site.

The security vulnerability

This plugin suffers from multiple critical vulnerabilities and could allow users to include arbitrary local .PHP files and inject a malicious SQL query into a WordPress database query execution.

The first vulnerability is Unauthenticated Local File Inclusion. This vulnerability allows any unauthenticated user to include arbitrary .PHP files that are available on the server. In the worst case, this could lead to a code execution if the user is able to fully or partially control some content on the .PHP files on the server. The second and third vulnerability is Subscriber+ SQL Injection. This vulnerability allows any authenticated user to inject a malicious SQL query into a WordPress database query execution. The described vulnerabilities were fixed in version 19.6.2 and assigned CVE-2024-31231, CVE-2024-31233, and CVE-2024-31234 respectively.

Unauthenticated Local File Inclusion

The underlying vulnerable code exists in the ajax_action_re_filterpost function:

function ajax_action_re_filterpost() {  
    check_ajax_referer( 'filter-nonce', 'security' );
    $args = (!empty($_POST['filterargs'])) ? rh_sanitize_multi_arrays($_POST['filterargs']) : array();
    $innerargs = (!empty($_POST['innerargs'])) ? rh_sanitize_multi_arrays($_POST['innerargs']) : array();
    $offset = (!empty($_POST['offset'])) ? intval( $_POST['offset'] ) : 0;
    $template = (!empty($_POST['template'])) ? sanitize_text_field( $_POST['template'] ) : '';
    $sorttype = (!empty($_POST['sorttype'])) ? rh_sanitize_multi_arrays( $_POST['sorttype'] ) : '';
    $tax = (!empty($_POST['tax'])) ? rh_sanitize_multi_arrays( $_POST['tax'] ) : '';
    $containerid = (!empty($_POST['containerid'])) ? sanitize_text_field( $_POST['containerid'] ) : '';

------------------ CUT HERE ------------------

    if ( $wp_query->have_posts() ) {
        while ($wp_query->have_posts() ) {
            $wp_query->the_post();
            ob_start();
            if(!empty($innerargs)) {extract($innerargs);}
            include(rh_locate_template('inc/parts/'.$template.'.php'));
            $i++;
            $response .= ob_get_clean();
        }
        wp_reset_query();
        if ($i >= $perpage){
            $response .='<div class="re_ajax_pagination"><span data-offset="'.$offsetnext.'" data-containerid="'.$containerid.'"'.$page_sorting.' class="re_ajax_pagination_btn def_btn">' . esc_html__('Next', 'rehub-theme') . '</span></div>';
        } 
    }           
    else {
        $response .= '<div class="clearfix flexbasisclear"><span class="no_more_posts">'.__('No more!', 'rehub-theme').'<span></div>';
    } 

The function itself is attached to the wp_ajax_nopriv_re_filterpost hook which can be accessed by unauthenticated users. Notice that the $template variable is constructed from $_POST['template'] parameter with the insufficient sanitize_text_field function. The sanitize_text_field itself is not enough to prevent a path traversal payload. The $template variable will be included using the include function and will go through the rh_locate_template function first. An unauthenticated user could just simply supply a path traversal payload to include arbitrary local .PHP files.

Subscriber+ SQL Injection

First, let's check the underlying vulnerable code on the REHub theme. It exists in the get_products_title_list function located inside of the rehub-elementor/abstracts/content-base-widget.php file:

public function get_products_title_list() {
    global $wpdb;

    //$post_types = get_post_types( array('public'   => true) );
    //$placeholdersformat = array_fill(0, count( $post_types ), '%s');
    //$postformat = implode(", ", $placeholdersformat);

    $query = [
        "select" => "SELECT SQL_CALC_FOUND_ROWS ID, post_title FROM {$wpdb->posts}",
        "where"  => "WHERE post_type IN ('post', 'product', 'blog', 'page')",
        "like"   => "AND post_title NOT LIKE %s",
        "offset" => "LIMIT %d, %d"
    ];

    $search_term = '';
    if ( ! empty( $_POST['search'] ) ) {
        $search_term = $wpdb->esc_like( $_POST['search'] ) . '%';
        $query['like'] = 'AND post_title LIKE %s';
    }

    $offset = 0;
    $search_limit = 100;
    if ( isset( $_POST['page'] ) && intval( $_POST['page'] ) && $_POST['page'] > 1 ) {
        $offset = $search_limit * absint( $_POST['page'] );
    }

    $final_query = $wpdb->prepare( implode(' ', $query ), $search_term, $offset, $search_limit );
    // Return saved values

    if ( ! empty( $_POST['saved'] ) && is_array( $_POST['saved'] ) ) {
        $saved_ids = $_POST['saved'];
        $placeholders = array_fill(0, count( $saved_ids ), '%d');
        $format = implode(', ', $placeholders);

        $new_query = [
            "select" => $query['select'],
            "where"  => $query['where'],
            "id"     => "AND ID IN( $format )",
            "order"  => "ORDER BY field(ID, " . implode(",", $saved_ids) . ")"
        ];

        $final_query = $wpdb->prepare( implode(" ", $new_query), $saved_ids );
    }

    $results = $wpdb->get_results( $final_query );
    $total_results = $wpdb->get_row("SELECT FOUND_ROWS() as total_rows;");
    $response_data = [
        'results'       => [],
        'total_count'   => $total_results->total_rows
    ];

    if ( $results ) {
        foreach ( $results as $result ) {
            $response_data['results'][] = [
                'id'    => $result->ID,
                'text'  => esc_html( $result->post_title )
            ];
        }
    }

    wp_send_json_success( $response_data );
}

Notice that the $saved_ids variable is constructed from $_POST['saved'] and used on the $new_query["order"] object without proper sanitization. The value then will be constructed on the $final_query variable and will be executed as a SQL query.

An identical case exists in the REHub Framework plugin, the underlying vulnerable code also exists in the get_products_title_list function:

public function get_products_title_list()
{
	global $wpdb;

	$query = [
		"select" => "SELECT SQL_CALC_FOUND_ROWS ID, post_title FROM {$wpdb->posts}",
		"where"  => "WHERE post_type IN ('post', 'product', 'blog', 'page')",
		"like"   => "AND post_title NOT LIKE %s",
		"offset" => "LIMIT %d, %d"
	];

	$search_term = '';
	if (!empty($_POST['search'])) {
		$search_term = $wpdb->esc_like($_POST['search']) . '%';
		$query['like'] = 'AND post_title LIKE %s';
	}

	$offset = 0;
	$search_limit = 100;
	if (isset($_POST['page']) && intval($_POST['page']) && $_POST['page'] > 1) {
		$offset = $search_limit * absint($_POST['page']);
	}

	$final_query = $wpdb->prepare(implode(' ', $query), $search_term, $offset, $search_limit);
	// Return saved values

	if (!empty($_POST['saved']) && is_array($_POST['saved'])) {
		$saved_ids = $_POST['saved'];
		$placeholders = array_fill(0, count($saved_ids), '%d');
		$format = implode(', ', $placeholders);

		$new_query = [
			"select" => $query['select'],
			"where"  => $query['where'],
			"id"     => "AND ID IN( $format )",
			"order"  => "ORDER BY field(ID, " . implode(",", $saved_ids) . ")"
		];

		$final_query = $wpdb->prepare(implode(" ", $new_query), $saved_ids);
	}

	$results = $wpdb->get_results($final_query);
	$total_results = $wpdb->get_row("SELECT FOUND_ROWS() as total_rows;");
	$response_data = [
		'results'       => [],
		'total_count'   => $total_results->total_rows
	];

	if ($results) {
		foreach ($results as $result) {
			$response_data['results'][] = [
				'value'    => $result->ID,
				'id'    => $result->ID,
				'label'  => esc_html($result->post_title)
			];
		}
	}

	wp_send_json_success($response_data);
}

Note that all of the vulnerabilities are reproducible on a default installation and activation of the REHub theme and REHub Framework plugin with a requirement of the Elementor plugin installation.

The patch

For the Unauthenticated Local File Inclusion vulnerability, the vendor decided to add sanitize_file_name function to sanitize the $template variable. The patch can be seen below:

For both of the Subscriber+ SQL Injection vulnerabilities, the vendor decided to apply an integer cast to the $saved_ids variable using intval.

Conclusion

The vulnerabilities discussed here underscore the importance of securing all aspects of a plugin, especially those designed for local file inclusion and SQL query execution.

In the context of SQL query execution, we recommend developers to force cast the value to integer if the intended value is indeed should be an integer value before constructing the value to the SQL query. We can also use $wpdb->prepare() statement with specifying "%d" as the input format.

In the context of local file inclusion, we recommend applying a sanitization using sanitize_file_name function to prevent path traversal and additionally apply a strict whitelist check to only allow certain files to be included.

Timeline

17 March, 2024We found the vulnerability and starting to create a reports.
18 March, 2024We reach out to the theme vendor regarding the discovered vulnerabilities.
19 March, 2024REHub theme and plugin version 19.6.2 released to patch the reported issues.
03 April , 2024Added the vulnerabilities to the Patchstack vulnerability database. Security advisory article published.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Critical Vulnerabilities Patched in REHub Theme and Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/critical-vulnerabilities-patched-in-rehub-theme-and-plugin/feed/ 0
Critical Vulnerabilities Patched in WordPress Automatic Plugin https://patchstack.com/articles/critical-vulnerabilities-patched-in-wordpress-automatic-plugin/ https://patchstack.com/articles/critical-vulnerabilities-patched-in-wordpress-automatic-plugin/#respond Tue, 19 Mar 2024 08:55:46 +0000 https://patchstack.com/?p=16998 This blog post is about the Automatic plugin vulnerabilities. If you're an Automatic user, please update the plugin to at least version 3.92.1. About the Automatic Plugin The plugin Automatic (premium version), which is estimated to have over 40,000 active installations, is known as the more popular automatic content posts plugin in WordPress. This plugin […]

The post Critical Vulnerabilities Patched in WordPress Automatic Plugin appeared first on Patchstack.

]]>

This blog post is about the Automatic plugin vulnerabilities. If you're an Automatic user, please update the plugin to at least version 3.92.1.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the Automatic Plugin

The plugin Automatic (premium version), which is estimated to have over 40,000 active installations, is known as the more popular automatic content posts plugin in WordPress. This plugin is developed by ValvePress.

This premium WordPress plugin has some features, one of which is to create posts from almost any website to WordPress automatically. It can import from popular sites like YouTube and Twitter utilizing the APIs or from almost any website of our choice using scraping modules. The plugin also now can generate content using OpenAI GPT.

The security vulnerability

This plugin suffers from multiple critical vulnerabilities and could allow any unauthenticated user to read local files and gain a full-scale SQL query execution on the WordPress site.

The first vulnerability is Unauthenticated Arbitrary SQL Execution. This vulnerability allows any unauthenticated user to fully control an SQL query that will be executed on the WordPress site. The second vulnerability is Unauthenticated Arbitrary File Download and SSRF. This vulnerability allows any unauthenticated user to read arbitrary local files and perform a Server-Side Request Forgery (SSRF) attack on the WordPress site server. The described vulnerabilities were fixed in version 3.92.1 and assigned CVE-2024-27956 and CVE-2024-27954 respectively.

Unauthenticated Arbitrary SQL Execution

The underlying vulnerability exists on inc/csv.php file:

<?php
require_once('../../../../wp-load.php');
global $wpdb;

 

  global $current_user;
  wp_get_current_user();

     //   echo user_login . "'s email address is: " . $current_user->user_pass;
 

//get admin pass for integrity check 


// extract query
$q = stripslashes($_POST['q']);
$auth = stripslashes($_POST['auth']);
$integ=stripslashes($_POST['integ']);

if(wp_automatic_trim($auth == '')){
	
	  echo 'login required';
	exit;
}

if(wp_automatic_trim($auth) != wp_automatic_trim($current_user->user_pass)){
	  echo 'invalid login';
	exit;
}

if(md5(wp_automatic_trim($q.$current_user->user_pass)) != $integ ){
	  echo 'Tampered query';
	exit;
}
 

$rows=$wpdb->get_results( $q);
$date=date("F j, Y, g:i a s");
$fname=md5($date);
header("Content-type: application/csv");
header("Content-Disposition: attachment; filename=$fname.csv");
header("Pragma: no-cache");
header("Expires: 0");

  echo "DATE,ACTION,DATA,KEYWORD \n";
foreach($rows as $row){
	
	$action=$row->action;
	if (stristr($action , 'New Comment Posted on :')){
			$action = 'Posted Comment';
		}elseif(stristr($action , 'approved')){
			$action = 'Approved Comment';
	}
	
	//format date
	$date=date('Y-n-j H:i:s',strtotime ($row->date));

	$data=$row->data;
	$keyword='';
	//filter the data strip keyword
	if(stristr($data,';')){
		$datas=explode(';',$row->data);
		$data=$datas[0];
		$keyword=$datas[1];
	}
	  echo "$date,$action,$data,$keyword \n";

}

//  echo "record1,$q,record3\n";

?>

We can see that we are able to supply an arbitrary SQL query on $q variable and it will be executed with $wpdb->get_results( $q). However, there are checks implemented using wp_automatic_trim($current_user->user_pass) and then md5(wp_automatic_trim($q.$current_user->user_pass)).

Let's see the content of wp_automatic_trim function:

function wp_automatic_trim($str)
{
	if (is_null($str)) {
		return '';
	} else {
		return trim($str);
	}
}

The function just as stated in the function name, performs a basic trim to the supplied value. The first check involves $current_user->user_pass value. This value would be an empty string if the file is accessed by an unauthenticated user. So, we can just supply an empty string to the $auth variable. For the second check, we just need to supply only the MD5 value of our supplied SQL query to the $integ since $current_user->user_pass is an empty string. However, before the two checks, there is a check of if(wp_automatic_trim($auth == '')) making us can't just input an empty string to the $auth. To bypass this, we can just supply a single whitespace (" ") to the $auth and we are able to achieve an arbitrary SQL query execution.

Unauthenticated Arbitrary File Download and SSRF

The underlying vulnerability exists on downloader.php file:

function curl_exec_follow( &$ch){

	$max_redir = 3;

	for ($i=0;$i<$max_redir;$i++){

		$exec=curl_exec($ch);
		$info = curl_getinfo($ch);

		
		if($info['http_code'] == 301 ||  $info['http_code'] == 302  ||  $info['http_code'] == 307 ){
				
			curl_setopt($ch, CURLOPT_URL, wp_automatic_trim($info['redirect_url']));
			$exec=curl_exec($ch);
				
		}else{
				
			//no redirect just return
			break;
				
		}


	}

	return $exec;

}

$link=$_GET['link'];//urldecode();
    $link=wp_automatic_str_replace('httpz','http',$link);
    //$link='http://ointmentdirectory.info/%E0%B8%81%E0%B8%B2%E0%B8%A3%E0%B9%81%E0%B8%AA%E0%B8%94%E0%B8%87%E0%B8%A0%E0%B8%B2%E0%B8%9E%E0%B8%99%E0%B8%B4%E0%B9%88%E0%B8%87-%E0%B8%97%E0%B8%AD%E0%B8%94%E0%B8%9B%E0%B8%A5%E0%B8%B2%E0%B9%80%E0%B8%9E';
    //  echo $link ;
    //exit ;
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, wp_automatic_trim($link));
    curl_setopt($ch, CURLOPT_HEADER, 1);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
    curl_setopt($ch,CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_REFERER, 'http://bing.com');
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8');
    curl_setopt($ch,CURLOPT_MAXREDIRS, 5); // Good leeway for redirections.
    curl_setopt($ch,CURLOPT_FOLLOWLOCATION, 0); // Many login forms redirect at least once.
    
    $exec=curl_exec_follow($ch);

    
    
    $res=array();
    //get the link 
    $curlinfo=curl_getinfo($ch);
    
     
	$original_link=$curlinfo['url'];
	$original_link=wp_automatic_str_replace("?hop=zzzzz",'',$original_link);
	$res['link']=$original_link;
	
	//get the title
	preg_match("/<title>(.*?)<\/title>/i",$exec,$matches );
	$ret=$matches[1];

	$res['title']=$ret;
	$res['status']='success';

	$ret = array();
	
	/*** a new dom object ***/
	$dom = new domDocument;
	
	/*** get the HTML (suppress errors) ***/
	@$dom->loadHTML($exec);
	
	/*** remove silly white space ***/
	$dom->preserveWhiteSpace = false;
	
	/*** get the links from the HTML ***/
	$text = $dom->getElementsByTagName('p');
	
	/*** loop over the links ***/
	foreach ($text as $tag)
	{
		$textContent = $tag->textContent;
	
		if(wp_automatic_trim($textContent) == '' || strlen($textContent) < 25 || stristr($textContent, 'HTTP') || stristr($textContent, '$')) continue;
		$ret[] = $textContent;
		
	}
	
	$res['text']=$ret;
	
	  echo json_encode($res);

	exit;
    
    @unlink('files/temp.html');
    $cont=curl_exec($ch);
    //$cont=file_get_contents($link);
    if (curl_error($ch)){
    	  echo 'Curl Error:error:'.curl_error($ch);
    }
    file_put_contents('files/temp.html',$link.$cont);
?>

The mentioned file can accessed by setting the wp_automatic GET parameter (thru the to $wp->query_vars) to "download" as stated in the wp_automatic_parse_request function:

function wp_automatic_parse_request($wp) {

	//secret word 
	$wp_automatic_secret = wp_automatic_trim(get_option('wp_automatic_cron_secret'));
	if(wp_automatic_trim($wp_automatic_secret) == '') $wp_automatic_secret = 'cron';
	
	// only process requests with "my-plugin=ajax-handler"
	if (array_key_exists('wp_automatic', $wp->query_vars)) {
			
		if($wp->query_vars['wp_automatic'] == $wp_automatic_secret){
			require_once(dirname(__FILE__) . '/cron.php');
			exit;

		}elseif ($wp->query_vars['wp_automatic'] == 'download'){
			require_once 'downloader.php';
			exit;
		}elseif ($wp->query_vars['wp_automatic'] == 'test'){
			require_once 'test.php';
			exit;
		}elseif($wp->query_vars['wp_automatic'] == 'show_ip'){
			$ch = curl_init();
			curl_setopt($ch, CURLOPT_HEADER,0);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
			curl_setopt($ch, CURLOPT_TIMEOUT,20);
			curl_setopt($ch, CURLOPT_REFERER, 'http://www.bing.com/');
			curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.8) Gecko/2009032609 Firefox/3.0.8');
			curl_setopt($ch, CURLOPT_MAXREDIRS, 5); // Good leeway for redirections.
			curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // Many login forms redirect at least once.
			
			//curl get
			$x='error';
			$url='https://www.showmyip.com/';
			
			curl_setopt($ch, CURLOPT_HTTPGET, 0);
			curl_setopt($ch, CURLOPT_URL, wp_automatic_trim($url));
			
			$exec=curl_exec($ch);
			$x=curl_error($ch);
			
			//<h2 id="ipv4">41.176.183.83</h2>
			if(strpos($exec,'<h2 id="ipv4">')){
				preg_match('{<h2 id="ipv4">(.*?)</h2>}', $exec , $ip_matches);
				print_r($ip_matches[1]);
			}else{
			
				echo $exec.$x;
			}
			exit;
		 
		
		}
	}
}
add_action('parse_request', 'wp_automatic_parse_request');

Back to the downloader.php file, we are able to supply an arbitrary URL or even local files on $_GET['link'] parameter and it will be fetched using cURL.

The patch

For the Unauthenticated Arbitrary SQL Execution vulnerability, the vendor decided to remove entirely the inc/csv.php file. For the Unauthenticated Arbitrary File Download and SSRF, the vendor decided to apply a nonce check (whereas the nonce value could only be fetched from a privileged user) and apply a check on the $link variable.

Conclusion

The vulnerabilities discussed here underscore the importance of securing all aspects of a plugin, especially those designed for SQL query execution and URL fetch. In the context of SQL query execution, we recommend developers to not provide a full-scale SQL query execution feature even for a high-privilege user such as an Administrator user. For the URL fetch process, we recommend applying permission and nonce check on the action and applying some checks and limitations to the supplied URL. For the best security practice, we recommend to use wp_safe_remote_* function to fetch the supplied URL by the user.

Timeline

25 February, 2024We found the vulnerability and reached out to the plugin vendor.
27 February, 2024Automatic version 3.92.1 released to patch the reported issues.
13 March, 2024Added the vulnerabilities to the Patchstack vulnerability database. Deployed vPatch rule to protect users.
19 March , 2024Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Critical Vulnerabilities Patched in WordPress Automatic Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/critical-vulnerabilities-patched-in-wordpress-automatic-plugin/feed/ 0
Critical SQL Injection Found in Porto Theme's Plugin https://patchstack.com/articles/critical-sql-injection-found-in-porto-themes-plugin/ https://patchstack.com/articles/critical-sql-injection-found-in-porto-themes-plugin/#respond Wed, 20 Dec 2023 09:21:10 +0000 https://patchstack.com/?p=16157 This blog post is about the Porto Theme's plugin vulnerability. If you're a Porto Theme user, please update the plugin to at least version 2.12.1. About the Porto Theme's Plugin The plugin Porto Theme - Functionality (premium version) is a required plugin for the Porto theme. The theme itself is estimated to have more than […]

The post Critical SQL Injection Found in Porto Theme's Plugin appeared first on Patchstack.

]]>
This blog post is about the Porto Theme's plugin vulnerability. If you're a Porto Theme user, please update the plugin to at least version 2.12.1.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

About the Porto Theme's Plugin

The plugin Porto Theme - Functionality (premium version) is a required plugin for the Porto theme. The theme itself is estimated to have more than 95,000 currently active installations. The Porto theme is known as the more popular premium multipurpose & WooCommerce Theme. This plugin is developed by p-themes.

Porto theme is an ultimate business & WooCommerce WordPress theme that is suitable for any business and WooCommerce site. Porto provides plenty of elements and powerful features that can configure all we want. Porto provides ultimate WooCommerce features with exclusive skins, layouts, and features.

The security vulnerability

This plugin suffers from an unauthenticated SQL injection vulnerability. This vulnerability allows any unauthenticated user to perform SQL injection. The described vulnerability was fixed in version 2.12.1 and assigned CVE-2023-48738.

Check this vulnerability in the Patchstack vulnerability database.

Unauthenticated SQL Injection

The underlying vulnerable code exists in the bulk_delete_critical function:

/**
 * Bulk delete the critical CSS.
 *
 * @since 2.3.0
 */
public function bulk_delete_critical() {

    if ( ! isset( $_GET['post'] ) ) {
        $this->redirect_critical_wizard();
    }

    $page_ids = wp_unslash( $_GET['post'] );

    foreach ( $page_ids as $key => $value ) {
        if ( 'homepage' == $value ) {
            unset( $page_ids[ $key ] );
            update_option( 'homepage_critical', '' );
            break;
        }
    }

    // Delete critical css
    global $wpdb;
    $page_ids = sanitize_text_field( implode( ',', $page_ids ) );
    $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->postmeta . " SET meta_value = '' WHERE meta_id IN ($page_ids)" ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared

    $this->redirect_critical_wizard();
}

This function handles the process of deletion of critical CSS, which is a feature that helps the user reduce the rendering time of the CSS file. The function can be called from the table_actions function:

/**
 * The Table Actions
 *
 * @since 2.3.0
 */
public function table_actions() {
    $action = '';
    if ( isset( $_REQUEST['action'] ) ) {
        if ( -1 !== $_REQUEST['action'] && '-1' !== $_REQUEST['action'] ) {
            $action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) );
        }
    }
    if ( isset( $_REQUEST['action2'] ) ) {
        if ( -1 !== $_REQUEST['action2'] && '-1' !== $_REQUEST['action2'] ) {
            $action = sanitize_text_field( wp_unslash( $_REQUEST['action2'] ) );
        }
    }

    if ( ! empty( $action ) ) {
        if ( 'porto_bulk_delete_critical' == $action ) {
            $this->bulk_delete_critical();
        } elseif ( 'delete_css' == $action ) {
            $this->delete_css();
        }
        return false;
    }
    if ( ( isset( $_REQUEST['page'] ) && false !== strpos( $_REQUEST['page'], 'porto' ) ) && ( isset( $_REQUEST['action2'] ) || isset( $_REQUEST['action'] ) ) ) {
        $referer = wp_get_referer();
        if ( $referer ) {
            wp_safe_redirect( $referer );
            die;
        }
    }
    return false;
}

The table_actions is just a function that handles some process on the critical CSS feature. The function actually is called from the init function:

/**
 * The init function.
 *
 * @since 2.3.0
 */
public function init() {
    global $porto_settings_optimize;
    if ( defined( 'PORTO_VERSION' ) && ! empty( $porto_settings_optimize['critical_css'] ) && is_admin() ) {
        add_action( 'admin_menu', array( $this, 'add_admin_menus' ) );
        $this->table_actions();
    }
}

This init function is hooked to the WordPress built-in init hook which is just a hook that runs after WordPress has finished loading but before any headers are sent. Given the condition, any unauthenticated user is able to execute the init function and eventually call bulk_delete_critical through the table_actions function. This is possible because there is no permission and nonce validation on the table_actions or bulk_delete_critical functions.

Notice that in the bulk_delete_critical function we can inject our SQL injection payload via the $page_ids variable.

Note that this vulnerability can be reproduced with an Unauthenticated user with the condition that the Critical CSS feature is enabled on the plugin settings.

The patch

The patch includes an implementation of permission and nonce validation on the table_actions function. For the vulnerable variable, force mapping the $page_ids variable to an integer value should be enough to prevent SQL injection. The patch can be seen below:

Conclusion

Always secure the SQL process in plugins or themes with proper function and implementation. Both are important since the usage of proper functions to prevent SQL Injection like esc_sql() and $wpdb->prepare alone are not enough to prevent SQL Injection if the usage implementation is not proper.

For a value that is intended to only contain an integer value, we recommend to implement intval to the variable so that it contains only valid integer values.

Timeline

1 August, 2023
We found the vulnerability and reached out to the vendor.

23 November, 2023
Published the vulnerabilities to the Patchstack vulnerability database (No reply from vendor).

25 November, 2023
Porto Theme - Functionality plugin version 2.12.1 released to patch the reported issue.

20 December, 2023
Security advisory article publicly released.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The post Critical SQL Injection Found in Porto Theme's Plugin appeared first on Patchstack.

]]>
https://patchstack.com/articles/critical-sql-injection-found-in-porto-themes-plugin/feed/ 0