Published : July 1, 2026

CVE-2026-13357: Houzez Property Feed <= 2.5.46 Authenticated (Administrator+) SQL Injection via 'orderby' Parameter PoC, Patch Analysis & Rule

Severity Medium (CVSS 4.9)
CWE 89
Vulnerable Version 2.5.46
Patched Version 2.5.47
Disclosed June 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-13357:

This vulnerability is an authenticated SQL Injection in the Houzez Property Feed plugin for WordPress (versions 2.5.46 and earlier). The flaw exists in the prepare_items() method of both the Houzez_Property_Feed_Admin_Logs_Export_Table class (in class-houzez-property-feed-admin-logs-export-table.php) and the Houzez_Property_Feed_Admin_Logs_Import_Table class (in class-houzez-property-feed-admin-logs-import-table.php). An attacker with Administrator-level access can exploit the ‘orderby’ GET parameter to inject arbitrary SQL. The CVSS score is 4.9 (Medium).

The root cause is improper handling of the $_GET[‘orderby’] and $_GET[‘order’] parameters. In the vulnerable code, at lines 204-205 (export table) and lines 178-179 (import table), the plugin uses sanitize_text_field() on the user-supplied ‘orderby’ parameter. This sanitization is insufficient for SQL use because it does not prevent SQL keywords or operators from being injected. The sanitized value is then directly interpolated into the SQL query string via concatenation: “$query .= ” ORDER BY $orderby $order”;” before $wpdb->prepare() is called. Since prepare() only parameterizes the appended LIMIT/OFFSET clause, the ORDER BY clause remains vulnerable to injection.

Exploitation is achieved by sending a crafted GET request to the WordPress admin area where the log tables are rendered. The specific parameters are ‘orderby’ and ‘order’ passed as GET parameters to a URL like: /wp-admin/admin.php?page=houzez-property-feed-export&tab=logs&orderby=INJECTION. An attacker can inject SQL payloads into the ‘orderby’ parameter to manipulate the query. For example: orderby=(CASE WHEN (SELECT 1 FROM wp_users WHERE user_login=’admin’ AND user_pass LIKE ‘a%’) THEN start_date ELSE NULL END). The ‘order’ parameter can be used to inject SQL as well. The injected SQL is executed in the context of the SQL query that retrieves log data from the database.

The patch introduces an allowlist approach. In both affected files (class-houzez-property-feed-admin-logs-export-table.php and class-houzez-property-feed-admin-logs-import-table.php), the code now defines an $allowed_orderby array that maps valid column names to themselves. The $orderby value is validated against this allowlist using isset() and defaults to ‘start_date’ if not in the allowlist. Similarly, the $order value is converted to uppercase, sanitized with sanitize_key(), and checked against the array(‘ASC’, ‘DESC’) before use. This ensures only predefined, safe values are used in the SQL query. The patch also improves output escaping in URL generation, using sanitize_key() and absint() for other parameters.

If exploited, this SQL injection can allow an authenticated Administrator to extract sensitive data from the WordPress database, including usernames, password hashes, email addresses, and other confidential information stored in custom tables. The attacker can potentially enumerate user credentials or retrieve arbitrary data via blind SQL injection techniques. Since the attacker already has administrative access, the impact is somewhat limited but still significant for data confidentiality, potentially exposing the entire database.

Differential between vulnerable and patched code

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

Code Diff
--- a/houzez-property-feed/houzez-property-feed.php
+++ b/houzez-property-feed/houzez-property-feed.php
@@ -3,7 +3,7 @@
  * Plugin Name: Houzez Property Feed
  * Plugin Uri: https://houzezpropertyfeed.com
  * Description: Automatically import properties to Houzez from estate agency CRMs and export to portals
- * Version: 2.5.46
+ * Version: 2.5.47
  * Author: PropertyHive
  * Author URI: https://wp-property-hive.com
  * License: GPLv3
@@ -19,7 +19,7 @@
     /**
      * @var string
      */
-    public $version = '2.5.46';
+    public $version = '2.5.47';

     /**
      * @var Houzez Property Feed The single instance of the class
--- a/houzez-property-feed/includes/class-houzez-property-feed-admin-logs-export-table.php
+++ b/houzez-property-feed/includes/class-houzez-property-feed-admin-logs-export-table.php
@@ -73,10 +73,26 @@
         {
             case 'col_log_date':
             {
-                $return = '<strong><a href="' . admin_url('admin.php?page=houzez-property-feed-export&tab=logs&action=view&log_id=' . $item->id . ( ( isset($_GET['export_id']) && !empty((int)$_GET['export_id']) ) ? '&export_id=' . (int)$_GET['export_id'] : '' ) . '&paged=' . ( isset($_GET['paged']) ? (int)$_GET['paged'] : '' ) . '&orderby=' . ( isset($_GET['orderby']) ? sanitize_text_field($_GET['orderby']) : '' ) . '&order=' . ( isset($_GET['order']) ? sanitize_text_field($_GET['order']) : '' ) ) . '">' . get_date_from_gmt( $item->start_date, "H:i:s jS F Y" ) . '</a></strong>';
+                $args = array(
+                    'page'    => 'houzez-property-feed-export',
+                    'tab'     => 'logs',
+                    'action'  => 'view',
+                    'log_id'  => absint($item->id),
+                    'paged'   => isset($_GET['paged']) ? absint($_GET['paged']) : 1,
+                    'orderby' => isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : '',
+                    'order'   => isset($_GET['order']) ? sanitize_key($_GET['order']) : '',
+                );
+
+                if (isset($_GET['export_id'])) {
+                    $args['export_id'] = absint($_GET['export_id']);
+                }
+
+                $url = add_query_arg($args, admin_url('admin.php'));
+
+                $return = '<strong><a href="' . esc_url($url) . '">' . esc_html(get_date_from_gmt( $item->start_date, "H:i:s jS F Y" )) . '</a></strong>';

                 $return .= '<div class="row-actions">
-                        <span class="edit"><a href="' . admin_url('admin.php?page=houzez-property-feed-export&tab=logs&action=view&log_id=' . $item->id . ( ( isset($_GET['export_id']) && !empty((int)$_GET['export_id']) ) ? '&export_id=' . (int)$_GET['export_id'] : '' ) . '&paged=' . ( isset($_GET['paged']) ? (int)$_GET['paged'] : '' ) . '&orderby=' . ( isset($_GET['orderby']) ? sanitize_text_field($_GET['orderby']) : '' ) . '&order=' . ( isset($_GET['order']) ? sanitize_text_field($_GET['order']) : '' ) ) . '" aria-label="' . __( 'View Log', 'houzezpropertyfeed' ) . '">' . __( 'View Log', 'houzezpropertyfeed' ) . '</a></span>
+                        <span class="edit"><a href="' . esc_url($url) . '" aria-label="' . esc_attr( __( 'View Log', 'houzezpropertyfeed' ) ) . '">' .esc_html( __( 'View Log', 'houzezpropertyfeed' )) . '</a></span>
                     </div>';

                 return $return;
@@ -116,11 +132,11 @@
                             $title = '(no title)';
                         }

-                        return '<a href="' . get_edit_post_link($explode_property_ids[0]) . '" target="_blank">' . $title . '</a>';
+                        return '<a href="' . esc_url(get_edit_post_link($explode_property_ids[0])) . '" target="_blank">' . esc_html($title) . '</a>';
                     }
                     else
                     {
-                        return count($explode_property_ids) . ' ' . __( 'properties', 'houzezpropertyfeed' );
+                        return esc_html(count($explode_property_ids) . ' ' . __( 'properties', 'houzezpropertyfeed' ));
                     }
                 }
                 return '-';
@@ -202,8 +218,14 @@

         $totalitems = $wpdb->get_var($query);

-        $orderby = (!empty($_GET['orderby'])) ? sanitize_text_field($_GET['orderby']) : 'start_date'; // default order
-        $order = (!empty($_GET['order'])) ? sanitize_text_field($_GET['order']) : 'asc'; // default order direction
+        $allowed_orderby = array(
+            'start_date' => 'start_date',
+        );
+        $orderby = (!empty($_GET['orderby'])) ? sanitize_text_field($_GET['orderby']) : 'start_date';
+        $orderby = isset($allowed_orderby[$orderby]) ? $allowed_orderby[$orderby] : 'start_date';
+
+        $order = !empty($_GET['order']) ? strtoupper(sanitize_key(wp_unslash($_GET['order']))) : 'ASC';
+        $order = in_array($order, array('ASC', 'DESC'), true) ? $order : 'ASC';

         $query = "SELECT
             id,
@@ -217,7 +239,7 @@
             $query .= " WHERE export_id = '" . $export_id . "' ";
         }
         $query .= " ORDER BY $orderby $order";
-        $query .= $wpdb->prepare(" LIMIT %d OFFSET %d", $per_page, ($current_page - 1) * $per_page);
+        $query .= $wpdb->prepare( " LIMIT %d OFFSET %d", absint($per_page), absint(($current_page - 1) * $per_page) );

         $this->items = $wpdb->get_results($query);

--- a/houzez-property-feed/includes/class-houzez-property-feed-admin-logs-import-table.php
+++ b/houzez-property-feed/includes/class-houzez-property-feed-admin-logs-import-table.php
@@ -52,10 +52,26 @@
         {
             case 'col_log_date':
             {
-                $return = '<strong><a href="' . admin_url('admin.php?page=houzez-property-feed-import&tab=logs&action=view&log_id=' . $item->id . ( ( isset($_GET['import_id']) && !empty((int)$_GET['import_id']) ) ? '&import_id=' . (int)$_GET['import_id'] : '' ) . '&paged=' . ( isset($_GET['paged']) ? (int)$_GET['paged'] : '' ) . '&orderby=' . ( isset($_GET['orderby']) ? sanitize_text_field($_GET['orderby']) : '' ) . '&order=' . ( isset($_GET['order']) ? sanitize_text_field($_GET['order']) : '' ) ) . '">' . get_date_from_gmt( $item->start_date, "H:i:s jS F Y" ) . '</a></strong>';
+                $args = array(
+                    'page'    => 'houzez-property-feed-import',
+                    'tab'     => 'logs',
+                    'action'  => 'view',
+                    'log_id'  => absint($item->id),
+                    'paged'   => isset($_GET['paged']) ? absint($_GET['paged']) : 1,
+                    'orderby' => isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : '',
+                    'order'   => isset($_GET['order']) ? sanitize_key($_GET['order']) : '',
+                );
+
+                if (isset($_GET['import_id'])) {
+                    $args['import_id'] = absint($_GET['import_id']);
+                }
+
+                $url = add_query_arg($args, admin_url('admin.php'));
+
+                $return = '<strong><a href="' . esc_url($url) . '">' . esc_html(get_date_from_gmt( $item->start_date, "H:i:s jS F Y" )) . '</a></strong>';

                 $return .= '<div class="row-actions">
-                        <span class="edit"><a href="' . admin_url('admin.php?page=houzez-property-feed-import&tab=logs&action=view&log_id=' . $item->id . ( ( isset($_GET['import_id']) && !empty((int)$_GET['import_id']) ) ? '&import_id=' . (int)$_GET['import_id'] : '' ) . '&paged=' . ( isset($_GET['paged']) ? (int)$_GET['paged'] : '' ) . '&orderby=' . ( isset($_GET['orderby']) ? sanitize_text_field($_GET['orderby']) : '' ) . '&order=' . ( isset($_GET['order']) ? sanitize_text_field($_GET['order']) : '' ) ) . '" aria-label="' . __( 'View Log', 'houzezpropertyfeed' ) . '">' . __( 'View Log', 'houzezpropertyfeed' ) . '</a></span>
+                        <span class="edit"><a href="' . esc_url($url) . '" aria-label="' . esc_attr(__( 'View Log', 'houzezpropertyfeed' )) . '">' . esc_html(__( 'View Log', 'houzezpropertyfeed' )) . '</a></span>
                     </div>';

                 return $return;
@@ -119,7 +135,7 @@
                     return '-';
                 }

-                return $format['name'];
+                return esc_html($format['name']);
             }
             default:
                 return print_r( $item, true ) ;
@@ -160,8 +176,14 @@

         $totalitems = $wpdb->get_var($query);

-        $orderby = (!empty($_GET['orderby'])) ? sanitize_text_field($_GET['orderby']) : 'start_date'; // default order
-        $order = (!empty($_GET['order'])) ? sanitize_text_field($_GET['order']) : 'asc'; // default order direction
+        $allowed_orderby = array(
+            'start_date' => 'start_date',
+        );
+        $orderby = (!empty($_GET['orderby'])) ? sanitize_text_field($_GET['orderby']) : 'start_date';
+        $orderby = isset($allowed_orderby[$orderby]) ? $allowed_orderby[$orderby] : 'start_date';
+
+        $order = !empty($_GET['order']) ? strtoupper(sanitize_key(wp_unslash($_GET['order']))) : 'ASC';
+        $order = in_array($order, array('ASC', 'DESC'), true) ? $order : 'ASC';

         $query = "SELECT
             id,
@@ -178,7 +200,7 @@
             $query .= " WHERE import_id = '" . (int)$_GET['import_id'] . "' ";
         }
         $query .= " ORDER BY $orderby $order";
-        $query .= $wpdb->prepare(" LIMIT %d OFFSET %d", $per_page, ($current_page - 1) * $per_page);
+        $query .= $wpdb->prepare( " LIMIT %d OFFSET %d", absint($per_page), absint(($current_page - 1) * $per_page) );

         $this->items = $wpdb->get_results($query);

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.