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.
All paid Patchstack users are protected from this vulnerability. Sign up for the free Community account first, to scan for vulnerabilities and apply protection for only $5 / site per month with Patchstack.
For plugin developers, we have security audit services and Enterprise API for hosting companies.
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
Help us make the Internet a safer place
Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.
- If you’re a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
- If you’re a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.