Published : June 21, 2026

CVE-2026-48967: Geo Mashup <= 1.13.19 Authenticated (Subscriber+) SQL Injection PoC, Patch Analysis & Rule

Plugin geo-mashup
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 1.13.19
Patched Version 1.13.20
Disclosed June 2, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-48967:

The Geo Mashup plugin for WordPress versions up to and including 1.13.19 contains a SQL Injection vulnerability. An authenticated attacker with subscriber-level access or higher can exploit this flaw. The vulnerability exists in the options saving functionality. It allows appending malicious SQL queries to extract sensitive database information. The CVSS score is 6.5 (Medium).

Root Cause:

The root cause is insufficient input sanitization and lack of parameterized queries in the options page rendering. In vulnerable versions (selected_tab` without sanitization. This value flows into SQL queries or template rendering contexts where SQL injection is possible. Additionally, the meta_key lookup query in `geo-mashup-db.php` (lines 2240-2250) uses `esc_sql()` which is insufficient for LIKE clause queries. The query concatenates user input directly: `HAVING meta_key LIKE ‘$like%’`. This permits SQL injection through the `q` GET parameter in postmeta form limit requests.

Exploitation:

An attacker with subscriber-level access crafts a request to the WordPress admin options page. The specific endpoint is `/wp-admin/admin.php?page=geo-mashup-settings` or similar admin page. The attacker sends a POST request with the action `geo_mashup_update_options`. The attacker includes crafted parameters in `$_GET[‘q’]` for the postmeta form. The SQL injection payload targets the `HAVING` clause meta_key lookup. A payload like `blah’ UNION SELECT user_login FROM wp_users WHERE id=1– -` appended to the `q` parameter allows extraction of sensitive data. The response returns the injected data in the output. Alternatively, submitting crafted options via POST allows options-based attacks.

Patch Analysis:

The patch in version 1.13.20 implements several fixes. In `OptionsPage.php`, the critical change moves the `check_admin_referer` and `current_user_can` checks to the beginning of the `render()` method, BEFORE `save()` is called. This ensures only administrators can submit options. In `TabsData.php`, the patch sanitizes `geo_mashup_selected_tab` using `intval()`, converting it to an integer. In `geo-mashup-db.php`, the meta_key query now uses `$wpdb->esc_like()` instead of `esc_sql()`, and the entire query is parameterized with `$wpdb->prepare()` using `%s` placeholder. The `LIMIT` clause is also parameterized with `%d`. The `set_null` field handling adds an allowlist of permitted column names (`$allowed_null_fields`) and uses `array_intersect` to filter user input.

Impact:

Successful exploitation allows an authenticated attacker with subscriber-level access to execute arbitrary SQL commands. This can extract usernames, password hashes, email addresses, and other sensitive data from the WordPress database. The attacker could also modify database content or perform privilege escalation. The vulnerability does not require special privileges beyond subscriber, making it accessible to many WordPress users. The injected SQL executes with the database user’s full permissions, which typically has SELECT, INSERT, UPDATE, and DELETE privileges across all WordPress tables.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/geo-mashup/geo-mashup-db.php
+++ b/geo-mashup/geo-mashup-db.php
@@ -1985,10 +1985,14 @@
 					if ( !is_array( $null_fields ) ) {
 						$null_fields = explode( ',', $null_fields );
 					}
-					$null_fields = array_map( function( $field ) {
-						return $field . "=NULL";
-					}, $null_fields );
-					$wpdb->query( $wpdb->prepare( "UPDATE $location_table SET " . implode( ',', $null_fields) . " WHERE id=%d", $db_location['id'] ) );
+					$allowed_null_fields = array( 'address', 'saved_name', 'geoname', 'postal_code', 'country_code', 'admin_code', 'sub_admin_code', 'locality_name' );
+					$null_fields = array_intersect( $null_fields, $allowed_null_fields );
+					if ( ! empty( $null_fields ) ) {
+						$null_fields = array_map( function( $field ) {
+							return $field . "=NULL";
+						}, $null_fields );
+						$wpdb->query( $wpdb->prepare( "UPDATE $location_table SET " . implode( ',', $null_fields) . " WHERE id=%d", $db_location['id'] ) );
+					}
 					unset( $location['set_null'] );
 				}

@@ -2240,14 +2244,17 @@
 			$limit = (int) apply_filters( 'postmeta_form_limit', 30 );
 			$terms = explode( ',', $_GET['q'] );
 			$stub = trim( array_pop( $terms ) );
-			$like = esc_sql( $stub );
-			$keys = $wpdb->get_col( "
+			$like = $wpdb->esc_like( $stub );
+			$keys = $wpdb->get_col( $wpdb->prepare( "
 				SELECT meta_key
 				FROM $wpdb->postmeta
 				GROUP BY meta_key
-				HAVING meta_key LIKE '$like%'
+				HAVING meta_key LIKE %s
 				ORDER BY meta_key
-				LIMIT $limit" );
+				LIMIT %d",
+				$like . '%',
+				$limit
+			) );
 			foreach( $keys as $key ) {
 				echo "$keyn";
 			}
--- a/geo-mashup/geo-mashup.php
+++ b/geo-mashup/geo-mashup.php
@@ -3,7 +3,7 @@
 Plugin Name: Geo Mashup
 Plugin URI: https://wordpress.org/plugins/geo-mashup/
 Description: Save location for posts and pages, or even users and comments. Display these locations on Google, Leaflet, and OSM maps. Make WordPress into your GeoCMS.
-Version: 1.13.19
+Version: 1.13.20
 Author: Dylan Kuhn
 Text Domain: GeoMashup
 Domain Path: /lang
@@ -256,7 +256,7 @@
 		define('GEO_MASHUP_DIRECTORY', dirname( GEO_MASHUP_PLUGIN_NAME ) );
 		define('GEO_MASHUP_URL_PATH', trim( plugin_dir_url( __FILE__ ), '/' ) );
 		define('GEO_MASHUP_MAX_ZOOM', 20);
-		define('GEO_MASHUP_VERSION', '1.13.19');
+		define('GEO_MASHUP_VERSION', '1.13.20');
 		define('GEO_MASHUP_DB_VERSION', '1.3');
 	}

--- a/geo-mashup/php/Admin/Settings/OptionsPage.php
+++ b/geo-mashup/php/Admin/Settings/OptionsPage.php
@@ -34,20 +34,27 @@
 		TestsPanel $tests_panel = null
 	) {
 		global $geo_mashup_options;
-		$this->options           = $options === null ? $geo_mashup_options : $options;
-		$this->db                = $db_adapter === null ? new DbAdapter() : $db_adapter;
-		$this->tabs              = $tabs === null ? new Tabs() : $tabs;
-		$this->overall_panel     = $overall_panel === null ? new OverallPanel() : $overall_panel;
-		$this->single_map_panel  = $single_map_panel === null ? new SingleMapPanel() : $single_map_panel;
-		$this->global_map_panel  = $global_map_panel === null ? new GlobalMapPanel() : $global_map_panel;
+		$this->options = $options === null ? $geo_mashup_options : $options;
+		$this->db = $db_adapter === null ? new DbAdapter() : $db_adapter;
+		$this->tabs = $tabs === null ? new Tabs() : $tabs;
+		$this->overall_panel = $overall_panel === null ? new OverallPanel() : $overall_panel;
+		$this->single_map_panel = $single_map_panel === null ? new SingleMapPanel() : $single_map_panel;
+		$this->global_map_panel = $global_map_panel === null ? new GlobalMapPanel() : $global_map_panel;
 		$this->context_map_panel = $context_map_panel === null ? new ContextMapPanel() : $context_map_panel;
-		$this->tests_panel       = $tests_panel === null ? new TestsPanel() : $tests_panel;
+		$this->tests_panel = $tests_panel === null ? new TestsPanel() : $tests_panel;
 	}

 	/**
 	 * @param array $submission Submitted data
 	 */
 	public function render( $submission ) {
+		if ( isset( $submission['submit'] ) ) {
+			check_admin_referer( 'geo-mashup-update-options' );
+		}
+
+		if ( ! current_user_can( 'manage_options' ) ) {
+			wp_die( 'Not Authorized' );
+		}

 		echo $this->save( $submission );
 		echo $this->duplicate_geodata();
@@ -58,15 +65,15 @@
 		echo $this->corrupt_options();
 		echo $this->validation_errors();

-		$data                         = new PageData();
-		$data->action                 = $_SERVER['REQUEST_URI'];
-		$data->view_activation_log    = isset($_GET['view_activation_log']);
-		$data->tabs_data              = TabsData::from_submission( $submission );
-		$data->overall_panel_data     = new OverallPanelData();
-		$data->single_map_panel_data  = new SingleMapPanelData();
-		$data->global_map_panel_data  = new GlobalMapPanelData();
+		$data = new PageData();
+		$data->action = $_SERVER['REQUEST_URI'];
+		$data->view_activation_log = isset( $_GET['view_activation_log'] );
+		$data->tabs_data = TabsData::from_submission( $submission );
+		$data->overall_panel_data = new OverallPanelData();
+		$data->single_map_panel_data = new SingleMapPanelData();
+		$data->global_map_panel_data = new GlobalMapPanelData();
 		$data->context_map_panel_data = new ContextMapPanelData();
-		$data->tests_panel_data       = TestsPanelData::from_submission( $submission );
+		$data->tests_panel_data = TestsPanelData::from_submission( $submission );

 		PageView::render(
 			$data,
@@ -84,12 +91,6 @@
 			return '';
 		}

-		check_admin_referer( 'geo-mashup-update-options' );
-
-		if ( ! current_user_can( 'manage_options' ) ) {
-			wp_die( 'Not Authorized' );
-		}
-
 		// Make missing array options empty
 		if ( empty( $submission['global_map']['add_map_type_control'] ) ) {
 			$submission['global_map']['add_map_type_control'] = array();
@@ -116,7 +117,7 @@
 		}

 		if ( isset( $submission['overall']['copy_geodata'] ) &&
-		     'true' !== $this->options->get( 'overall', 'copy_geodata' ) ) {
+			'true' !== $this->options->get( 'overall', 'copy_geodata' ) ) {
 			$this->activated_copy_geodata = true;
 		}

@@ -125,9 +126,9 @@
 		$saved = $this->options->save();
 		if ( $saved ) {
 			return '<div class="updated fade"><p>' .
-			       __( 'Options updated.  Browser or server caching may delay updates for recently viewed maps.',
-				       'GeoMashup' ) .
-			       '</p></div>';
+				__( 'Options updated.  Browser or server caching may delay updates for recently viewed maps.',
+					'GeoMashup' ) .
+				'</p></div>';
 		}


@@ -141,8 +142,8 @@
 		$this->db->duplicate_geodata();

 		return '<div class="updated fade"><p>' .
-		       __( 'Copied existing geodata, see log for details.', 'GeoMashup' ) .
-		       '</p></div>';
+			__( 'Copied existing geodata, see log for details.', 'GeoMashup' ) .
+			'</p></div>';
 	}

 	private function bulk_reverse_geocode( $submission ) {
@@ -165,13 +166,13 @@

 		if ( ! function_exists( 'mb_check_encoding' ) ) {
 			return '<div class="updated fade">' .
-			       printf(
-				       __( '%s Multibyte string functions %s are not installed.', 'GeoMashup' ),
-				       '<a href="http://www.php.net/manual/en/mbstring.installation.php" title="">',
-				       '</a>'
-			       ) . ' ' .
-			       _e( 'Geocoding and other web services may not work properly.', 'GeoMashup' ) .
-			       '</div>';
+				printf(
+					__( '%s Multibyte string functions %s are not installed.', 'GeoMashup' ),
+					'<a href="http://www.php.net/manual/en/mbstring.installation.php" title="">',
+					'</a>'
+				) . ' ' .
+				_e( 'Geocoding and other web services may not work properly.', 'GeoMashup' ) .
+				'</div>';
 		}

 		$test_transient = get_transient( 'geo_mashup_test' );
@@ -179,9 +180,9 @@
 			unset( $submission['geo_mashup_run_tests'] );

 			return '<div class="updated fade">' .
-			       _e( 'WordPress transients may not be working. Try deactivating or reconfiguring caching plugins.',
-				       'GeoMashup' ) .
-			       ' <a href="https://github.com/cyberhobo/wordpress-geo-mashup/issues/425">issue 425</a></div>';
+				_e( 'WordPress transients may not be working. Try deactivating or reconfiguring caching plugins.',
+					'GeoMashup' ) .
+				' <a href="https://github.com/cyberhobo/wordpress-geo-mashup/issues/425">issue 425</a></div>';
 		}

 		// load tests
@@ -191,7 +192,7 @@
 	private function migrate() {
 		if ( $this->db->is_install_needed() && $this->db->install() ) {
 			return '<div class="updated fade"><p>' .
-			       __( 'Database upgraded, see log for details.', 'GeoMashup' ) . '</p></div>';
+				__( 'Database upgraded, see log for details.', 'GeoMashup' ) . '</p></div>';
 		}

 		return '';
@@ -210,10 +211,10 @@
 	}

 	private function corrupt_options() {
-		if ( ! empty ( $this->options->corrupt_options ) ) {
+		if ( ! empty( $this->options->corrupt_options ) ) {
 			// Options didn't load correctly
 			$message = __( 'Saved options may be corrupted, try updating again. Corrupt values: ', 'GeoMashup' ) .
-			           '<code>' . $this->options->corrupt_options . '</code>';
+				'<code>' . $this->options->corrupt_options . '</code>';

 			return '<div class="updated"><p>' . $message . '</p></div>';
 		}
@@ -222,13 +223,13 @@
 	}

 	private function validation_errors() {
-		if ( empty ( $this->options->validation_errors ) ) {
+		if ( empty( $this->options->validation_errors ) ) {
 			return '';
 		}
 		$html = '<div class="updated"><p>' .
-		        __( 'Some invalid options will not be used. If you've just upgraded, do an update to initialize new options.',
-			        'GeoMashup' ) .
-		        '<ul>';
+			__( 'Some invalid options will not be used. If you've just upgraded, do an update to initialize new options.',
+				'GeoMashup' ) .
+			'<ul>';
 		foreach ( $this->options->validation_errors as $message ) {
 			$html .= "<li>$message</li>";
 		}
--- a/geo-mashup/php/Admin/Settings/TabsData.php
+++ b/geo-mashup/php/Admin/Settings/TabsData.php
@@ -12,8 +12,8 @@
 	public $include_tests;

 	public static function from_submission( $submission ) {
-		$instance                = new self();
-		$instance->selected_tab  = isset($submission['geo_mashup_selected_tab']) ? $submission['geo_mashup_selected_tab'] : '0';
+		$instance = new self();
+		$instance->selected_tab = isset( $submission['geo_mashup_selected_tab'] ) ? intval( $submission['geo_mashup_selected_tab'] ) : 0;
 		$instance->include_tests = defined( 'WP_DEBUG' ) && WP_DEBUG;

 		return $instance;
--- a/geo-mashup/vendor/composer/installed.php
+++ b/geo-mashup/vendor/composer/installed.php
@@ -1,9 +1,9 @@
 <?php return array(
     'root' => array(
         'name' => 'cyberhobo/wordpress-geo-mashup',
-        'pretty_version' => '1.13.19',
-        'version' => '1.13.19.0',
-        'reference' => 'dc9a83950897592ee664acc7dea71600e68ea932',
+        'pretty_version' => '1.13.20',
+        'version' => '1.13.20.0',
+        'reference' => 'cf7fc716c461c154d71cc8cfd4ba8043cca736d1',
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
         'aliases' => array(),
@@ -11,9 +11,9 @@
     ),
     'versions' => array(
         'cyberhobo/wordpress-geo-mashup' => array(
-            'pretty_version' => '1.13.19',
-            'version' => '1.13.19.0',
-            'reference' => 'dc9a83950897592ee664acc7dea71600e68ea932',
+            'pretty_version' => '1.13.20',
+            'version' => '1.13.20.0',
+            'reference' => 'cf7fc716c461c154d71cc8cfd4ba8043cca736d1',
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',
             'aliases' => array(),

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School