Commit 451678360fd for woocommerce

commit 451678360fd2014cd1a1bcde4117ad6868309d15
Author: Chi-Hsuan Huang <chihsuan.tw@gmail.com>
Date:   Fri Jul 3 19:33:09 2026 +0800

    Use HTTPS for all geolocation IP lookup and geo-IP requests (#66223)

    * Use HTTPS for all geolocation IP lookup and geo-IP requests

    * Add changelog entry for geolocation HTTPS fix

    * refactor: retain ip-api.com parsing for BC and scope geolocation test filters

    Keep the ip-api.com switch branch so extensions re-adding it via the woocommerce_geolocation_geoip_apis filter still parse correctly, and merge the identical ipinfo.io/country.is cases. Use targeted remove_filter instead of remove_all_filters in the API fallback test to avoid clearing global pre_http_request guards.

diff --git a/plugins/woocommerce/changelog/46099-fix-geolocation-https b/plugins/woocommerce/changelog/46099-fix-geolocation-https
new file mode 100644
index 00000000000..237578697b5
--- /dev/null
+++ b/plugins/woocommerce/changelog/46099-fix-geolocation-https
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Use HTTPS for all geolocation IP-lookup and geo-IP requests. Switch the IP-lookup services (ipify, ipecho, ident, tnedi) to HTTPS and replace the HTTP-only ip-api.com geo-IP provider with the HTTPS-based country.is, so visitor IP addresses are never sent over unencrypted connections.
diff --git a/plugins/woocommerce/includes/class-wc-geolocation.php b/plugins/woocommerce/includes/class-wc-geolocation.php
index 5a0140b317d..e2a86df839d 100644
--- a/plugins/woocommerce/includes/class-wc-geolocation.php
+++ b/plugins/woocommerce/includes/class-wc-geolocation.php
@@ -47,10 +47,10 @@ class WC_Geolocation {
 	 * @var array
 	 */
 	private static $ip_lookup_apis = array(
-		'ipify'  => 'http://api.ipify.org/',
-		'ipecho' => 'http://ipecho.net/plain',
-		'ident'  => 'http://ident.me',
-		'tnedi'  => 'http://tnedi.me',
+		'ipify'  => 'https://api.ipify.org/',
+		'ipecho' => 'https://ipecho.net/plain',
+		'ident'  => 'https://ident.me',
+		'tnedi'  => 'https://tnedi.me',
 	);

 	/**
@@ -60,7 +60,7 @@ class WC_Geolocation {
 	 */
 	private static $geoip_apis = array(
 		'ipinfo.io'  => 'https://ipinfo.io/%s/json',
-		'ip-api.com' => 'http://ip-api.com/json/%s',
+		'country.is' => 'https://api.country.is/%s',
 	);

 	/**
@@ -310,10 +310,12 @@ class WC_Geolocation {
 				if ( ! is_wp_error( $response ) && $response['body'] ) {
 					switch ( $service_name ) {
 						case 'ipinfo.io':
+						case 'country.is':
 							$data         = json_decode( $response['body'] );
 							$country_code = isset( $data->country ) ? $data->country : '';
 							break;
 						case 'ip-api.com':
+							// Not a default provider (HTTP-only), but retained so extensions that re-add it via the woocommerce_geolocation_geoip_apis filter keep working.
 							$data         = json_decode( $response['body'] );
 							$country_code = isset( $data->countryCode ) ? $data->countryCode : ''; // @codingStandardsIgnoreLine
 							break;
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/geolocation/class-wc-test-gelocation.php b/plugins/woocommerce/tests/legacy/unit-tests/geolocation/class-wc-test-gelocation.php
index 18ba769ff65..3336473d2db 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/geolocation/class-wc-test-gelocation.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/geolocation/class-wc-test-gelocation.php
@@ -49,4 +49,77 @@ class WC_Tests_Geolocation extends WC_Unit_Test_Case {
 		$this->assertEquals( '2620:0:ccc::2', WC_Geolocation::get_ip_address() );
 		unset( $_SERVER['REMOTE_ADDR'] );
 	}
+
+	/**
+	 * Read the value of a private static property on WC_Geolocation.
+	 *
+	 * @param string $property Property name.
+	 * @return mixed
+	 */
+	private function get_private_static( $property ) {
+		$ref = new ReflectionProperty( 'WC_Geolocation', $property );
+		$ref->setAccessible( true );
+
+		return $ref->getValue();
+	}
+
+	/**
+	 * @testdox Every geo-IP lookup endpoint should use HTTPS so visitor IP addresses are never sent unencrypted.
+	 */
+	public function test_geoip_apis_all_use_https() {
+		$apis = $this->get_private_static( 'geoip_apis' );
+
+		foreach ( $apis as $service_name => $endpoint ) {
+			$this->assertStringStartsWith( 'https://', $endpoint, "Geo-IP service {$service_name} must use HTTPS" );
+		}
+
+		$this->assertArrayNotHasKey( 'ip-api.com', $apis, 'The HTTP-only ip-api.com provider should no longer be used' );
+	}
+
+	/**
+	 * @testdox Every IP-lookup endpoint should use HTTPS so visitor IP addresses are never sent unencrypted.
+	 */
+	public function test_ip_lookup_apis_all_use_https() {
+		$apis = $this->get_private_static( 'ip_lookup_apis' );
+
+		foreach ( $apis as $service_name => $endpoint ) {
+			$this->assertStringStartsWith( 'https://', $endpoint, "IP-lookup service {$service_name} must use HTTPS" );
+		}
+	}
+
+	/**
+	 * @testdox Geolocating via the API should parse the country code and only request HTTPS endpoints.
+	 */
+	public function test_geolocate_via_api_uses_https_and_parses_country() {
+		$ip_address    = '8.8.8.8';
+		$requested_url = '';
+
+		delete_transient( 'geoip_' . $ip_address );
+
+		// Force the database lookup to return nothing so the API fallback runs.
+		$force_empty_geolocation = function ( $data ) {
+			$data['country'] = '';
+			return $data;
+		};
+		add_filter( 'woocommerce_get_geolocation', $force_empty_geolocation, 999 );
+
+		// Intercept the outgoing request; the body is valid for both configured providers.
+		$intercept_request = function ( $pre, $args, $url ) use ( &$requested_url ) {
+			$requested_url = $url;
+			return array(
+				'body'     => wp_json_encode( array( 'country' => 'US' ) ),
+				'response' => array( 'code' => 200 ),
+			);
+		};
+		add_filter( 'pre_http_request', $intercept_request, 10, 3 );
+
+		$geolocation = WC_Geolocation::geolocate_ip( $ip_address, false, true );
+
+		remove_filter( 'pre_http_request', $intercept_request, 10 );
+		remove_filter( 'woocommerce_get_geolocation', $force_empty_geolocation, 999 );
+		delete_transient( 'geoip_' . $ip_address );
+
+		$this->assertEquals( 'US', $geolocation['country'], 'The country code from the API response should be returned' );
+		$this->assertStringStartsWith( 'https://', $requested_url, 'The geolocation request must be made over HTTPS' );
+	}
 }