CVE-2019-13575: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (CWE-89)
value: A SQL injection vulnerability exists in WPEverest Everest Forms plugin for WordPress through 1.4.9. Successful exploitation of this vulnerability would allow a remote attacker to execute arbitrary SQL commands on the affected system via includes/evf-entry-functions.php
CWE description: The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.
CVSS v3 base score: 9.8
Published: 2019-07-18T15:15Z
Repository: https://github.com/wpeverest/everest-forms
Language: PHP
Files changed: includes/evf-entry-functions.php
Commit date: 2019-07-12 11:39:21 +0545
Commit message: Fix - Security issue reported by Tin Duong on entries SQL query

Diff: diff --git a/includes/evf-entry-functions.php b/includes/evf-entry-functions.php
index d45af67..21c7cc9 100644
--- a/includes/evf-entry-functions.php
+++ b/includes/evf-entry-functions.php
@@ -73,59 +73,49 @@ function evf_search_entries( $args ) {
 			'order'   => 'DESC',
 			'orderby' => 'entry_id',
 		)
 	);
 
+	$statuses     = array_keys( evf_get_entry_statuses() );
+	$valid_fields = array( 'date', 'form_id', 'title', 'status' );
+
 	// Check if form ID is valid for entries.
 	if ( ! array_key_exists( $args['form_id'], evf_get_all_forms() ) ) {
 		return array();
 	}
 
-	$orderby       = isset( $args['orderby'] ) ? sanitize_key( $args['orderby'] ) : 'entry_id';
-	$order         = "ORDER BY {$orderby} " . esc_sql( strtoupper( $args['order'] ) );
-	$limit         = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : '';
-	$offset        = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : '';
-	$status        = ! empty( $args['status'] ) ? "AND `status` = '" . sanitize_key( $args['status'] ) . "'" : '';
-	$search        = ! empty( $args['search'] ) ? "AND `meta_value` LIKE '%" . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . "%'" : '';
-	$include       = ! empty( $args['form_id'] ) ? "AND `form_id` = '" . absint( $args['form_id'] ) . "'" : '';
-	$exclude       = '';
-	$date_created  = '';
-	$date_modified = '';
-
-	if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) {
-		$args['after']  = empty( $args['after'] ) ? '0000-00-00' : $args['after'];
-		$args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before'];
-
-		$date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')";
+	$query   = array();
+	$query[] = "SELECT DISTINCT {$wpdb->prefix}evf_entries.entry_id FROM {$wpdb->prefix}evf_entries INNER JOIN {$wpdb->prefix}evf_entrymeta WHERE {$wpdb->prefix}evf_entries.entry_id = {$wpdb->prefix}evf_entrymeta.entry_id";
+
+	if ( ! empty( $args['search'] ) ) {
+		$like    = '%' . $wpdb->esc_like( $args['search'] ) . '%';
+		$query[] = $wpdb->prepare( 'AND meta_value LIKE %s', $like );
 	}
 
-	if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) {
-		$args['modified_after']  = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after'];
-		$args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before'];
+	if ( ! empty( $args['form_id'] ) ) {
+		$query[] = $wpdb->prepare( 'AND form_id = %d', absint( $args['form_id'] ) );
+	}
 
-		$date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')";
+	if ( ! empty( $args['status'] ) ) {
+		$query[] = $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : 'publish' );
 	}
 
-	$query = trim(
-		"
-		SELECT DISTINCT {$wpdb->prefix}evf_entries.entry_id
-		FROM {$wpdb->prefix}evf_entries
-		INNER JOIN {$wpdb->prefix}evf_entrymeta
-		WHERE {$wpdb->prefix}evf_entries.entry_id = {$wpdb->prefix}evf_entrymeta.entry_id
-		{$status}
-		{$search}
-		{$include}
-		{$exclude}
-		{$date_created}
-		{$date_modified}
-		{$order}
-		{$limit}
-		{$offset}
-		"
-	);
+	$orderby     = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'entry_id';
+	$order       = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
+	$orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" );
+	$query[]     = "ORDER BY {$orderby_sql}";
+
+	if ( -1 < $args['limit'] ) {
+		$query[] = $wpdb->prepare( 'LIMIT %d', absint( $args['limit'] ) );
+	}
+
+	if ( 0 < $args['offset'] ) {
+		$query[] = $wpdb->prepare( 'LIMIT %d', absint( $args['offset'] ) );
+	}
 
-	$results = $wpdb->get_results( $query ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
+	// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
+	$results = $wpdb->get_results( implode( ' ', $query ), ARRAY_A );
 
 	$ids = wp_list_pluck( $results, 'entry_id' );
 
 	return $ids;
 }

Vulnerable code: 	$orderby       = isset( $args['orderby'] ) ? sanitize_key( $args['orderby'] ) : 'entry_id';
	$order         = "ORDER BY {$orderby} " . esc_sql( strtoupper( $args['order'] ) );
	$limit         = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : '';
	$offset        = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : '';
	$status        = ! empty( $args['status'] ) ? "AND `status` = '" . sanitize_key( $args['status'] ) . "'" : '';
	$search        = ! empty( $args['search'] ) ? "AND `meta_value` LIKE '%" . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . "%'" : '';
	$include       = ! empty( $args['form_id'] ) ? "AND `form_id` = '" . absint( $args['form_id'] ) . "'" : '';
	$exclude       = '';
	$date_created  = '';
	$date_modified = '';

	if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) {
		$args['after']  = empty( $args['after'] ) ? '0000-00-00' : $args['after'];
		$args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before'];

		$date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')";
	if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) {
		$args['modified_after']  = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after'];
		$args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before'];
		$date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')";
	$query = trim(
		"
		SELECT DISTINCT {$wpdb->prefix}evf_entries.entry_id
		FROM {$wpdb->prefix}evf_entries
		INNER JOIN {$wpdb->prefix}evf_entrymeta
		WHERE {$wpdb->prefix}evf_entries.entry_id = {$wpdb->prefix}evf_entrymeta.entry_id
		{$status}
		{$search}
		{$include}
		{$exclude}
		{$date_created}
		{$date_modified}
		{$order}
		{$limit}
		{$offset}
		"
	);
	$results = $wpdb->get_results( $query ); // WPCS: cache ok, DB call ok, unprepared SQL ok.
Fixed code: 	$statuses     = array_keys( evf_get_entry_statuses() );
	$valid_fields = array( 'date', 'form_id', 'title', 'status' );

	$query   = array();
	$query[] = "SELECT DISTINCT {$wpdb->prefix}evf_entries.entry_id FROM {$wpdb->prefix}evf_entries INNER JOIN {$wpdb->prefix}evf_entrymeta WHERE {$wpdb->prefix}evf_entries.entry_id = {$wpdb->prefix}evf_entrymeta.entry_id";

	if ( ! empty( $args['search'] ) ) {
		$like    = '%' . $wpdb->esc_like( $args['search'] ) . '%';
		$query[] = $wpdb->prepare( 'AND meta_value LIKE %s', $like );
	if ( ! empty( $args['form_id'] ) ) {
		$query[] = $wpdb->prepare( 'AND form_id = %d', absint( $args['form_id'] ) );
	}
	if ( ! empty( $args['status'] ) ) {
		$query[] = $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : 'publish' );
	$orderby     = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'entry_id';
	$order       = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC';
	$orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" );
	$query[]     = "ORDER BY {$orderby_sql}";

	if ( -1 < $args['limit'] ) {
		$query[] = $wpdb->prepare( 'LIMIT %d', absint( $args['limit'] ) );
	}

	if ( 0 < $args['offset'] ) {
		$query[] = $wpdb->prepare( 'LIMIT %d', absint( $args['offset'] ) );
	}
	// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
	$results = $wpdb->get_results( implode( ' ', $query ), ARRAY_A );