Commit 33d5065ceca for woocommerce

commit 33d5065ceca66aa2b862b243fd5f2f186e48f812
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date:   Wed Oct 7 14:06:44 2026 +0200

    Remove non-allowlisted query parameters from analytics URLs (#69526)

    * Remove non-allowlisted query parameters from analytics URLs

    Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>

    * Remove a stale PHPStan baseline entry

    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

    ---------

    Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com>
    Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

diff --git a/packages/php/woocommerce-analytics/changelog/fix-analytics-url-query-strings b/packages/php/woocommerce-analytics/changelog/fix-analytics-url-query-strings
new file mode 100644
index 00000000000..672a953d2aa
--- /dev/null
+++ b/packages/php/woocommerce-analytics/changelog/fix-analytics-url-query-strings
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fixed
+
+Remove non-allowlisted query parameters and fragments from analytics document location and referrer properties.
diff --git a/packages/php/woocommerce-analytics/composer.json b/packages/php/woocommerce-analytics/composer.json
index 079ed0b87e8..64f59e92a55 100644
--- a/packages/php/woocommerce-analytics/composer.json
+++ b/packages/php/woocommerce-analytics/composer.json
@@ -10,7 +10,8 @@
 		"automattic/jetpack-connection": "^8.1 || ^9.0",
 		"automattic/jetpack-constants": "^3.0 || ^4.0",
 		"automattic/jetpack-device-detection": "^3.4 || ^4.0",
-		"automattic/block-delimiter": "^0.3 || ^0.4"
+		"automattic/block-delimiter": "^0.3 || ^0.4",
+		"automattic/tracks-shared-utils": "^1.0"
 	},
 	"require-dev": {
 		"yoast/phpunit-polyfills": "^4.0.0",
diff --git a/packages/php/woocommerce-analytics/composer.lock b/packages/php/woocommerce-analytics/composer.lock
index de77e78c2c3..0ae2f7bcdea 100644
--- a/packages/php/woocommerce-analytics/composer.lock
+++ b/packages/php/woocommerce-analytics/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "60ed9225503f99d3adb184cf6d7223a5",
+    "content-hash": "e78b6681ee9e0c0a47559de3dd00738b",
     "packages": [
         {
             "name": "automattic/block-delimiter",
@@ -621,6 +621,50 @@
                 "source": "https://github.com/Automattic/jetpack-status/tree/v6.4.0"
             },
             "time": "2026-08-19T19:42:20+00:00"
+        },
+        {
+            "name": "automattic/tracks-shared-utils",
+            "version": "v1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Automattic/tracks-shared-utils.git",
+                "reference": "6828997db5c76b1e810535c1d2729355b095aa91"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Automattic/tracks-shared-utils/zipball/6828997db5c76b1e810535c1d2729355b095aa91",
+                "reference": "6828997db5c76b1e810535c1d2729355b095aa91",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+                "phpcompatibility/php-compatibility": "^9.3",
+                "phpunit/phpunit": "^8.5 || ^9.6",
+                "squizlabs/php_codesniffer": "^3.7"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "php/src/functions.php"
+                ],
+                "psr-4": {
+                    "Automattic\\TracksSharedUtils\\": "php/src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "GPL-2.0-or-later"
+            ],
+            "description": "Shared allowlist and sanitization for URLs sent to Tracks.",
+            "homepage": "https://github.com/Automattic/tracks-shared-utils",
+            "support": {
+                "issues": "https://github.com/Automattic/tracks-shared-utils/issues",
+                "source": "https://github.com/Automattic/tracks-shared-utils"
+            },
+            "time": "2026-09-25T19:43:46+00:00"
         }
     ],
     "packages-dev": [
diff --git a/packages/php/woocommerce-analytics/package.json b/packages/php/woocommerce-analytics/package.json
index c99f112a73b..13e17b0d638 100644
--- a/packages/php/woocommerce-analytics/package.json
+++ b/packages/php/woocommerce-analytics/package.json
@@ -28,6 +28,7 @@
 		"watch": "pnpm build --watch"
 	},
 	"dependencies": {
+		"@automattic/tracks-shared-utils": "^1.0.0",
 		"debug": "4.4.3"
 	},
 	"devDependencies": {
diff --git a/packages/php/woocommerce-analytics/src/class-wc-analytics-tracking.php b/packages/php/woocommerce-analytics/src/class-wc-analytics-tracking.php
index 48a303b0253..b858d9ddfea 100644
--- a/packages/php/woocommerce-analytics/src/class-wc-analytics-tracking.php
+++ b/packages/php/woocommerce-analytics/src/class-wc-analytics-tracking.php
@@ -14,6 +14,8 @@ use Automattic\Jetpack\Device_Detection;
 use Automattic\Jetpack\Device_Detection\User_Agent_Info;
 use Automattic\Woocommerce_Analytics;
 use WP_Error;
+use function Automattic\TracksSharedUtils\sanitize_url;
+use function Automattic\TracksSharedUtils\url_props;

 /**
  * WooCommerce Analytics Tracking class
@@ -609,8 +611,9 @@ class WC_Analytics_Tracking {
 			$event_properties = array_slice( $event_properties, 0, self::MAX_CLIENT_PROPERTIES_PER_EVENT, true );
 		}

-		$values = array();
-		$costs  = array();
+		$values         = array();
+		$costs          = array();
+		$url_properties = array_fill_keys( url_props(), true );

 		foreach ( $event_properties as $key => $value ) {
 			// Dropped, not truncated: two long names could truncate to the same key.
@@ -618,6 +621,13 @@ class WC_Analytics_Tracking {
 				continue;
 			}

+			if ( isset( $url_properties[ $key ] ) ) {
+				if ( ! is_string( $value ) ) {
+					continue;
+				}
+				$value = sanitize_url( $value );
+			}
+
 			// Arrays are flattened later by get_properties(); bound their members too.
 			if ( is_array( $value ) ) {
 				$value = array_map(
@@ -894,17 +904,17 @@ class WC_Analytics_Tracking {
 			'_via_ua' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? $clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
 			'_via_ip' => self::get_user_ip_address(),
 			'_lg'     => isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ), 0, 5 ) : '',
-			'_dr'     => isset( $_SERVER['HTTP_REFERER'] ) ? $clean( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			'_dr'     => isset( $_SERVER['HTTP_REFERER'] ) && is_string( $_SERVER['HTTP_REFERER'] ) ? sanitize_url( sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) ) : '', // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
 		);

 		// Build the document location URL.
-		$uri         = isset( $_SERVER['REQUEST_URI'] ) ? $clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-		$host        = isset( $_SERVER['HTTP_HOST'] ) ? $clean( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-		$data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) ? $clean( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+		$uri         = isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+		$host        = isset( $_SERVER['HTTP_HOST'] ) && is_string( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+		$data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) && is_string( $_SERVER['REQUEST_SCHEME'] ) ? sanitize_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

 		// Add _via_ref (referrer) for backward compatibility.
 		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-		$data['_via_ref'] = isset( $_SERVER['HTTP_REFERER'] ) ? $clean( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';
+		$data['_via_ref'] = isset( $_SERVER['HTTP_REFERER'] ) && is_string( $_SERVER['HTTP_REFERER'] ) ? sanitize_url( sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) ) : '';

 		// Headers are caller-supplied, and the referer lands here twice. Uncapped, one
 		// long Referer pushes the finished URL past MAX_PIXEL_URL_LENGTH and costs the
diff --git a/packages/php/woocommerce-analytics/src/client/analytics.ts b/packages/php/woocommerce-analytics/src/client/analytics.ts
index a95e96daac0..a58b2749436 100644
--- a/packages/php/woocommerce-analytics/src/client/analytics.ts
+++ b/packages/php/woocommerce-analytics/src/client/analytics.ts
@@ -2,6 +2,7 @@
  * External dependencies
  */
 import debugFactory from 'debug';
+import { sanitizeUrl } from '@automattic/tracks-shared-utils';
 /**
  * Internal dependencies
  */
@@ -202,10 +203,10 @@ export class Analytics {
 		eventProperties._sy = sy !== undefined ? sy : 0;

 		if ( document.location !== undefined ) {
-			eventProperties._dl = document.location.toString();
+			eventProperties._dl = sanitizeUrl( document.location.toString() );
 		}
 		if ( document.referrer !== undefined ) {
-			eventProperties._dr = document.referrer;
+			eventProperties._dr = sanitizeUrl( document.referrer );
 		}
 	};

diff --git a/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Reserved_Props_Test.php b/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Reserved_Props_Test.php
index 09762b2d232..423198d7804 100644
--- a/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Reserved_Props_Test.php
+++ b/packages/php/woocommerce-analytics/tests/php/WC_Analytics_Tracking_Reserved_Props_Test.php
@@ -128,6 +128,22 @@ class WC_Analytics_Tracking_Reserved_Props_Test extends BaseTestCase {
 		$this->assertNotContains( '_dr', $reserved );
 	}

+	/**
+	 * @testdox Request-derived URLs retain only shared allowlisted query parameters.
+	 */
+	public function test_server_details_retain_only_shared_allowlisted_query_parameters(): void {
+		$_SERVER['REQUEST_SCHEME'] = 'https';
+		$_SERVER['HTTP_HOST']      = 'example.com';
+		$_SERVER['REQUEST_URI']    = '/wp-json/wc/v3/products?utm_source=google&orderby=price&aff=partner&utm_campaign=spring&per_page=20';
+		$_SERVER['HTTP_REFERER']   = 'https://example.com/wp-admin/admin.php?page=wc-admin&utm_medium=referral#activity';
+
+		$details = WC_Analytics_Tracking::get_server_details();
+
+		$this->assertSame( 'https://example.com/wp-json/wc/v3/products?utm_source=google&aff=partner&utm_campaign=spring', $details['_dl'] );
+		$this->assertSame( 'https://example.com/wp-admin/admin.php?page=wc-admin&utm_medium=referral', $details['_dr'] );
+		$this->assertSame( 'https://example.com/wp-admin/admin.php?page=wc-admin&utm_medium=referral', $details['_via_ref'] );
+	}
+
 	/**
 	 * The strip removes reserved names and leaves everything else — including
 	 * arbitrary event-specific properties — untouched.
@@ -694,9 +710,9 @@ class WC_Analytics_Tracking_Reserved_Props_Test extends BaseTestCase {
 	}

 	/**
-	 * Keep typical client payloads unchanged.
+	 * @testdox Keep typical client payloads while retaining only attribution parameters.
 	 */
-	public function test_a_realistic_client_payload_is_not_capped(): void {
+	public function test_a_realistic_client_payload_retains_only_attribution_parameters(): void {
 		$properties = array(
 			'pi'  => 731,
 			'pn'  => 'Some Reasonably Long Product Name With Words',
@@ -705,10 +721,13 @@ class WC_Analytics_Tracking_Reserved_Props_Test extends BaseTestCase {
 			'pp'  => 115.81,
 			'_lg' => 'en-GB',
 			'_dl' => 'https://example.com/product/some-reasonably-long-product-slug/?utm_source=x',
-			'_dr' => 'https://example.com/shop/page/3/',
+			'_dr' => 'https://example.com/shop/page/3/?ref=email#products',
 		);
+		$expected        = $properties;
+		$expected['_dl'] = 'https://example.com/product/some-reasonably-long-product-slug/?utm_source=x';
+		$expected['_dr'] = 'https://example.com/shop/page/3/?ref=email';

-		$this->assertSame( $properties, WC_Analytics_Tracking::sanitize_client_properties( $properties ) );
+		$this->assertSame( $expected, WC_Analytics_Tracking::sanitize_client_properties( $properties ) );
 	}

 	/**
@@ -874,9 +893,9 @@ class WC_Analytics_Tracking_Reserved_Props_Test extends BaseTestCase {
 	}

 	/**
-	 * Trim long referrers without dropping the event.
+	 * @testdox Strip long referrer query strings without dropping the event.
 	 */
-	public function test_a_long_referer_costs_its_own_tail_not_the_event(): void {
+	public function test_a_long_referer_query_is_stripped_without_dropping_the_event(): void {
 		$_COOKIE['tk_ai']        = 'test-visitor-id-1234567890ab';
 		$_SERVER['HTTP_REFERER'] = 'https://example.com/?q=' . str_repeat( 'a', 5000 );
 		$this->reset_pixel_batch_queue();
@@ -888,22 +907,24 @@ class WC_Analytics_Tracking_Reserved_Props_Test extends BaseTestCase {
 		$props = $this->get_queued_pixel_props();

 		$this->assertSame( '42', $props['pi'] ?? null, 'The event payload must survive intact.' );
-		$this->assertStringEndsWith( '…', $props['_dr'] ?? '', 'The referer is what gets trimmed.' );
+		$this->assertSame( 'https://example.com/', $props['_dr'] ?? null );
+		$this->assertSame( 'https://example.com/', $props['_via_ref'] ?? null );

 		$this->reset_pixel_batch_queue();
 	}

 	/**
-	 * Preserve common ad-click landing URLs.
+	 * @testdox Preserve allowlisted parameters in common ad-click landing URLs.
 	 */
-	public function test_an_ad_click_landing_url_survives_untouched(): void {
-		$url = 'https://example.com/product-category/clothing/mens-shirts/?utm_source=google&utm_medium=cpc&utm_campaign=spring&gclid=Cj0KCQjw1viWBhD0ARIsAAM_oKnLQ8example1234567890abcdefghij&fbclid=IwAR2example1234567890abcdefghijklmnop';
+	public function test_an_ad_click_landing_url_preserves_allowlisted_parameters(): void {
+		$url      = 'https://example.com/product-category/clothing/mens-shirts/?utm_source=google&utm_medium=cpc&utm_campaign=spring&color=blue&gclid=Cj0KCQjw1viWBhD0ARIsAAM_oKnLQ8example1234567890abcdefghij&fbclid=IwAR2example1234567890abcdefghijklmnop';
+		$expected = 'https://example.com/product-category/clothing/mens-shirts/?utm_source=google&utm_medium=cpc&utm_campaign=spring&gclid=Cj0KCQjw1viWBhD0ARIsAAM_oKnLQ8example1234567890abcdefghij&fbclid=IwAR2example1234567890abcdefghijklmnop';

 		$this->assertGreaterThan( 200, mb_strlen( $url ), 'A fixture under the old cap would prove nothing.' );

 		$sanitized = WC_Analytics_Tracking::sanitize_client_properties( array( '_dl' => $url ) );

-		$this->assertSame( $url, $sanitized['_dl'] ?? null );
+		$this->assertSame( $expected, $sanitized['_dl'] ?? null );
 	}

 	/**
diff --git a/plugins/woocommerce/changelog/fix-analytics-url-query-strings b/plugins/woocommerce/changelog/fix-analytics-url-query-strings
new file mode 100644
index 00000000000..702b59a56c5
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-analytics-url-query-strings
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Remove non-allowlisted query parameters and fragments from analytics document location and referrer properties.
diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json
index 9f7422d0a20..4754e88ad7e 100644
--- a/plugins/woocommerce/composer.json
+++ b/plugins/woocommerce/composer.json
@@ -50,6 +50,7 @@
 		"automattic/jetpack-config": "3.0.0",
 		"automattic/jetpack-connection": "^6.11.1",
 		"automattic/jetpack-constants": "^3.0.1",
+		"automattic/tracks-shared-utils": "^1.0",
 		"composer/installers": "^1.9",
 		"maxmind-db/reader": "^1.11",
 		"opis/json-schema": "*",
diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock
index bcba9a2d236..23afdb9b503 100644
--- a/plugins/woocommerce/composer.lock
+++ b/plugins/woocommerce/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "7de5869e1b3a961f31d4e8427317c288",
+    "content-hash": "d04c0430452f13dd4fe7bc11bce08ce8",
     "packages": [
         {
             "name": "automattic/block-delimiter",
@@ -664,6 +664,50 @@
             },
             "time": "2025-10-13T20:12:54+00:00"
         },
+        {
+            "name": "automattic/tracks-shared-utils",
+            "version": "v1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Automattic/tracks-shared-utils.git",
+                "reference": "6828997db5c76b1e810535c1d2729355b095aa91"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Automattic/tracks-shared-utils/zipball/6828997db5c76b1e810535c1d2729355b095aa91",
+                "reference": "6828997db5c76b1e810535c1d2729355b095aa91",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "require-dev": {
+                "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+                "phpcompatibility/php-compatibility": "^9.3",
+                "phpunit/phpunit": "^8.5 || ^9.6",
+                "squizlabs/php_codesniffer": "^3.7"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "php/src/functions.php"
+                ],
+                "psr-4": {
+                    "Automattic\\TracksSharedUtils\\": "php/src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "GPL-2.0-or-later"
+            ],
+            "description": "Shared allowlist and sanitization for URLs sent to Tracks.",
+            "homepage": "https://github.com/Automattic/tracks-shared-utils",
+            "support": {
+                "issues": "https://github.com/Automattic/tracks-shared-utils/issues",
+                "source": "https://github.com/Automattic/tracks-shared-utils"
+            },
+            "time": "2026-09-25T19:43:46+00:00"
+        },
         {
             "name": "composer/installers",
             "version": "v1.12.0",
diff --git a/plugins/woocommerce/includes/tracks/class-wc-tracks.php b/plugins/woocommerce/includes/tracks/class-wc-tracks.php
index d2395ad9aff..5c9a8f06366 100644
--- a/plugins/woocommerce/includes/tracks/class-wc-tracks.php
+++ b/plugins/woocommerce/includes/tracks/class-wc-tracks.php
@@ -64,11 +64,11 @@ class WC_Tracks {
 		$data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
 		$data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
 		$data['_lg']     = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '';
-		$data['_dr']     = isset( $_SERVER['HTTP_REFERER'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';
+		$data['_dr']     = isset( $_SERVER['HTTP_REFERER'] ) && is_string( $_SERVER['HTTP_REFERER'] ) ? \Automattic\TracksSharedUtils\sanitize_url( sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) ) : '';

-		$uri         = isset( $_SERVER['REQUEST_URI'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
-		$host        = isset( $_SERVER['HTTP_HOST'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
-		$data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri : '';
+		$uri         = isset( $_SERVER['REQUEST_URI'] ) && is_string( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
+		$host        = isset( $_SERVER['HTTP_HOST'] ) && is_string( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
+		$data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) && is_string( $_SERVER['REQUEST_SCHEME'] ) ? \Automattic\TracksSharedUtils\sanitize_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri ) : '';

 		return $data;
 	}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 948d44c9e2b..c0c3b363487 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -31509,12 +31509,6 @@ parameters:
 			count: 1
 			path: includes/tracks/class-wc-tracks-footer-pixel.php

-		-
-			message: '#^Binary operation "\." between array\|string and ''\://'' results in an error\.$#'
-			identifier: binaryOp.invalid
-			count: 1
-			path: includes/tracks/class-wc-tracks.php
-
 		-
 			message: '#^Method WC_Tracks\:\:track_woocommerce_allow_tracking_toggled\(\) has no return type specified\.$#'
 			identifier: missingType.return
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-tracks-test.php b/plugins/woocommerce/tests/php/includes/class-wc-tracks-test.php
index 1f49fda0f65..a42f10004f2 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-tracks-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-tracks-test.php
@@ -18,6 +18,27 @@ class WC_Tracks_Test extends \WC_Unit_Test_Case {
 		include_once WC_ABSPATH . 'includes/tracks/class-wc-tracks-event.php';
 	}

+	/**
+	 * @testdox Server details retain only shared allowlisted query parameters.
+	 */
+	public function test_server_details_retain_only_shared_allowlisted_query_parameters(): void {
+		$server_snapshot = $_SERVER;
+
+		try {
+			$_SERVER['REQUEST_SCHEME'] = 'https';
+			$_SERVER['HTTP_HOST']      = 'example.com';
+			$_SERVER['REQUEST_URI']    = '/wp-json/wc/v3/products?utm_source=google&orderby=price&aff=partner&utm_campaign=spring&per_page=20';
+			$_SERVER['HTTP_REFERER']   = 'https://example.com/wp-admin/admin.php?page=wc-admin&utm_medium=referral#activity';
+
+			$details = WC_Tracks::get_server_details();
+
+			$this->assertSame( 'https://example.com/wp-json/wc/v3/products?utm_source=google&aff=partner&utm_campaign=spring', $details['_dl'] );
+			$this->assertSame( 'https://example.com/wp-admin/admin.php?page=wc-admin&utm_medium=referral', $details['_dr'] );
+		} finally {
+			$_SERVER = $server_snapshot;
+		}
+	}
+
 	/**
 	 * Test that custom event properties are returned when passed.
 	 */
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dd58ae2839f..99690fdf591 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2484,6 +2484,9 @@ importers:

   packages/php/woocommerce-analytics:
     dependencies:
+      '@automattic/tracks-shared-utils':
+        specifier: ^1.0.0
+        version: 1.0.0
       debug:
         specifier: 4.4.3
         version: 4.4.3(supports-color@9.4.0)
@@ -4432,6 +4435,9 @@ packages:
       react-dom: ^18.2.0
       redux: ^4.2.1

+  '@automattic/tracks-shared-utils@1.0.0':
+    resolution: {integrity: sha512-yHXhAmfEK64a8LE1kweoKGgIW5XWe96mbTF99XD5lgRQcZVvWOSqiauQ7Q48OQVXu9K+isWLSXYxk5nGbdN+FA==, tarball: https://registry.npmjs.org/@automattic/tracks-shared-utils/-/tracks-shared-utils-1.0.0.tgz}
+
   '@automattic/typography@1.0.0':
     resolution: {integrity: sha512-TnT+vPaNUXQYwDsPCPxhNY0d4LnOKvrb0SizUCC5iybo5sfOlX/rYalGDyz6nPQDF0EBaQwMf7qhVsflFR0cBg==}

@@ -19625,6 +19631,8 @@ snapshots:
       - '@types/react'
       - supports-color

+  '@automattic/tracks-shared-utils@1.0.0': {}
+
   '@automattic/typography@1.0.0': {}

   '@automattic/viewport-react@1.0.1(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':