Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/mapsvg-lite-interactive-vector-maps/mapsvg.php
+++ b/mapsvg-lite-interactive-vector-maps/mapsvg.php
@@ -3,7 +3,7 @@
Plugin Name: MapSVG Lite
Plugin URI: https://mapsvg.com
Description: Any maps with database integration, filters and search. Use included maps or draw your own. Create vector maps, Google maps, image maps, floor plans, store locators.
-Version: 8.14.0
+Version: 8.14.1
Requires at least: 5.0
Requires PHP: 7.4
Author: Northern Lights Production
@@ -25,7 +25,7 @@
define('MAPSVG_API_URL', 'https://mapsvg.com/dashboard/api');
define('MAPSVG_PLAN', 'mapsvg-lite');
/** MapSVG version number */
-define('MAPSVG_VERSION', '8.14.0');
+define('MAPSVG_VERSION', '8.14.1');
/** Prefix for MapSVG tables in the database */
define('MAPSVG_PREFIX', 'mapsvg6_');
--- a/mapsvg-lite-interactive-vector-maps/php/Admin/Admin.php
+++ b/mapsvg-lite-interactive-vector-maps/php/Admin/Admin.php
@@ -232,7 +232,7 @@
"svgFileLastChanged" => $map->svgFileLastChanged,
"options" => array(
"regionPrefix" => isset($map->options["regionPrefix"]) ? $map->options["regionPrefix"] : "",
- "loadingText" => isset($map->options["loadingText"]) ? $map->options["loadingText"] : "Loading map..."
+ "loadingText" => isset($map->options["loadingText"]) ? Map::sanitizeLoadingTextForOutput($map->options["loadingText"]) : "Loading map..."
)
),
'user' => array(
--- a/mapsvg-lite-interactive-vector-maps/php/Core/Repository.php
+++ b/mapsvg-lite-interactive-vector-maps/php/Core/Repository.php
@@ -920,11 +920,14 @@
*/
public function getDistinctValues($fieldName)
{
- $db = Database::get();
$table = $this->getTableName();
- // Escape field name for safety
- $field = esc_sql($fieldName);
- $sql = "SELECT DISTINCT `$field` FROM `$table` WHERE `$field` IS NOT NULL AND `$field` != ''";
+ if (!Utils::isTableColumn($table, $fieldName)) {
+ return [];
+ }
+
+ $db = Database::get();
+ // Column/table names verified via DESCRIBE + identifier checks.
+ $sql = "SELECT DISTINCT `{$fieldName}` FROM `{$table}` WHERE `{$fieldName}` IS NOT NULL AND `{$fieldName}` != ''";
$results = $db->get_col($sql, 0);
return $results ? $results : [];
}
--- a/mapsvg-lite-interactive-vector-maps/php/Core/Utils.php
+++ b/mapsvg-lite-interactive-vector-maps/php/Core/Utils.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace MapSVG;
+
+/**
+ * Shared SQL / identifier safety helpers.
+ */
+class Utils
+{
+ /**
+ * Cached column names per table for the current request.
+ *
+ * @var array<string, string[]>
+ */
+ private static $tableColumns = [];
+
+ /**
+ * True if value matches a safe WP slug / REST path segment (a-zA-Z0-9_-).
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ public static function isSafeSlug($value): bool
+ {
+ return is_string($value) && $value !== '' && preg_match('/^[a-zA-Z0-9_-]+$/', $value) === 1;
+ }
+
+ /**
+ * True if value is a safe SQL identifier (letters, digits, underscore).
+ *
+ * @param mixed $value
+ * @return bool
+ */
+ public static function isSafeSqlIdentifier($value): bool
+ {
+ return is_string($value) && $value !== '' && preg_match('/^[a-zA-Z0-9_]+$/', $value) === 1;
+ }
+
+ /**
+ * Returns true when $fieldName is an existing column on $tableName.
+ * Columns are read from the DB (not hardcoded).
+ *
+ * @param string $tableName Full table name (e.g. wp_posts, wp_mapsvg6_objects_1).
+ * @param string $fieldName Column name to check.
+ * @return bool
+ */
+ public static function isTableColumn($tableName, $fieldName): bool
+ {
+ if (!self::isSafeSqlIdentifier($tableName) || !self::isSafeSqlIdentifier($fieldName)) {
+ return false;
+ }
+
+ if (!isset(self::$tableColumns[$tableName])) {
+ $db = Database::get();
+ // DESCRIBE first column is Field name
+ $columns = $db->get_col("DESCRIBE `{$tableName}`", 0);
+ self::$tableColumns[$tableName] = is_array($columns) ? $columns : [];
+ }
+
+ return in_array($fieldName, self::$tableColumns[$tableName], true);
+ }
+}
--- a/mapsvg-lite-interactive-vector-maps/php/Domain/Map/DOMElement.php
+++ b/mapsvg-lite-interactive-vector-maps/php/Domain/Map/DOMElement.php
@@ -15,7 +15,7 @@
public function render() {
$attributesString = '';
foreach ($this->attributes as $key => $value) {
- $attributesString .= " $key="$value"";
+ $attributesString .= ' ' . $key . '="' . esc_attr((string)$value) . '"';
}
return "<{$this->tag}{$attributesString}>{$this->content}</{$this->tag}>";
}
--- a/mapsvg-lite-interactive-vector-maps/php/Domain/Map/Map.php
+++ b/mapsvg-lite-interactive-vector-maps/php/Domain/Map/Map.php
@@ -308,6 +308,52 @@
$this->optionsBroken = $value;
}
/**
+ * Sanitize loadingText on write.
+ * Users with unfiltered_html may store limited HTML (no JS); others get plain text.
+ *
+ * @param mixed $text
+ * @return string
+ */
+ public static function sanitizeLoadingText($text)
+ {
+ if (!is_string($text)) {
+ return '';
+ }
+ if (current_user_can('unfiltered_html')) {
+ return wp_kses_post($text);
+ }
+ return sanitize_text_field($text);
+ }
+
+ /**
+ * Sanitize loadingText for output / in-memory use (safe HTML, no JS).
+ *
+ * @param mixed $text
+ * @return string
+ */
+ public static function sanitizeLoadingTextForOutput($text)
+ {
+ if (!is_string($text)) {
+ return '';
+ }
+ return wp_kses_post($text);
+ }
+
+ /**
+ * Apply write-time sanitization to map options that can contain HTML.
+ *
+ * @param array $options
+ * @return array
+ */
+ public static function sanitizeOptionsForWrite(array $options)
+ {
+ if (isset($options['loadingText'])) {
+ $options['loadingText'] = self::sanitizeLoadingText($options['loadingText']);
+ }
+ return $options;
+ }
+
+ /**
* Sets map options
* @param $options
*/
@@ -327,6 +373,10 @@
}
}
+ if (is_array($options) && isset($options['loadingText'])) {
+ $options['loadingText'] = self::sanitizeLoadingTextForOutput($options['loadingText']);
+ }
+
$this->options = $options;
if ($this->options && is_array($this->options) && isset($this->options['source'])) {
--- a/mapsvg-lite-interactive-vector-maps/php/Domain/Map/MapController.php
+++ b/mapsvg-lite-interactive-vector-maps/php/Domain/Map/MapController.php
@@ -220,6 +220,9 @@
$mapsRepository = RepositoryFactory::get("map");
$map["options"] = json_decode($map["options"], true);
+ if (is_array($map["options"])) {
+ $map["options"] = Map::sanitizeOptionsForWrite($map["options"]);
+ }
// Validate map data
try {
@@ -583,6 +586,16 @@
$mapData['options'] = str_replace("!mapsvg-encoded-int", "int(11)", $mapData['options']);
}
+ if (isset($mapData['options']) && is_string($mapData['options'])) {
+ $decodedOptions = json_decode($mapData['options'], true);
+ if (is_array($decodedOptions)) {
+ $mapData['options'] = $decodedOptions;
+ }
+ }
+ if (isset($mapData['options']) && is_array($mapData['options'])) {
+ $mapData['options'] = Map::sanitizeOptionsForWrite($mapData['options']);
+ }
+
// Validate map data
try {
self::validate($mapData);
--- a/mapsvg-lite-interactive-vector-maps/php/Domain/PostTypes/PostTypesRepository.php
+++ b/mapsvg-lite-interactive-vector-maps/php/Domain/PostTypes/PostTypesRepository.php
@@ -104,16 +104,26 @@
/**
* Retrieves distinct values for a given field name from published posts.
- *
+ *
* @param string $fieldName The name of the field to retrieve distinct values for.
+ * @param string $post_type
* @return array An array of distinct values for the specified field.
*/
public function getFieldValues($fieldName, $post_type)
{
$db = Database::get();
- $post_type = esc_sql($post_type);
- $results = $db->get_col("SELECT DISTINCT " . esc_sql($fieldName) . " FROM " . $db->posts() . " WHERE post_status='publish' AND post_type='$post_type'", 0);
- return $results;
+ $postsTable = $db->posts();
+ if (!Utils::isSafeSlug($post_type) || !Utils::isTableColumn($postsTable, $fieldName)) {
+ return [];
+ }
+
+ // Column name is verified against DESCRIBE; post_type is bound via prepare.
+ $sql = $db->prepare(
+ "SELECT DISTINCT `{$fieldName}` FROM `{$postsTable}` WHERE post_status = 'publish' AND post_type = %s",
+ $post_type
+ );
+ $results = $db->get_col($sql, 0);
+ return $results ? $results : [];
}
/**
@@ -124,13 +134,19 @@
*/
public function getTaxonomyValues($name)
{
+ if (!Utils::isSafeSlug($name)) {
+ return [];
+ }
+
$db = Database::get();
- $taxonomy = esc_sql($name);
- $sql = "SELECT DISTINCT t.name FROM {$db->prefix}terms t
+ $sql = $db->prepare(
+ "SELECT DISTINCT t.name FROM {$db->prefix}terms t
INNER JOIN {$db->prefix}term_taxonomy tt ON t.term_id = tt.term_id
INNER JOIN {$db->prefix}term_relationships tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
- INNER JOIN {$db->posts()} p ON tr.object_id = p.ID
- WHERE tt.taxonomy = '$taxonomy' AND p.post_status = 'publish'";
+ INNER JOIN {$db->posts} p ON tr.object_id = p.ID
+ WHERE tt.taxonomy = %s AND p.post_status = 'publish'",
+ $name
+ );
$results = $db->get_col($sql, 0);
return $results ? $results : [];
}
@@ -143,11 +159,17 @@
*/
public function getMetaValues($name)
{
+ if (!Utils::isSafeSlug($name)) {
+ return [];
+ }
+
$db = Database::get();
- $meta_key = esc_sql($name);
- $sql = "SELECT DISTINCT pm.meta_value FROM {$db->postmeta} pm
- INNER JOIN {$db->posts()} p ON pm.post_id = p.ID
- WHERE pm.meta_key = '$meta_key' AND p.post_status = 'publish' AND pm.meta_value IS NOT NULL AND pm.meta_value != ''";
+ $sql = $db->prepare(
+ "SELECT DISTINCT pm.meta_value FROM {$db->postmeta} pm
+ INNER JOIN {$db->posts} p ON pm.post_id = p.ID
+ WHERE pm.meta_key = %s AND p.post_status = 'publish' AND pm.meta_value IS NOT NULL AND pm.meta_value != ''",
+ $name
+ );
$results = $db->get_col($sql, 0);
// Unserialize meta values if needed and flatten arrays to scalars
--- a/mapsvg-lite-interactive-vector-maps/php/Domain/SVGFile/SVGFile.php
+++ b/mapsvg-lite-interactive-vector-maps/php/Domain/SVGFile/SVGFile.php
@@ -8,37 +8,75 @@
{
public function __construct($file)
{
- // Check for path traversal
+ if (!is_array($file)) {
+ throw new Exception('Invalid file data', 400);
+ }
+
+ // Check for path traversal / extension on existing file references
if (isset($file['relativeUrl'])) {
$relativePath = $file['relativeUrl'];
if (strpos($relativePath, '../') !== false || strpos($relativePath, '..\') !== false) {
throw new Exception('Invalid file path: path traversal detected', 400);
}
- // Ensure .svg extension
- if (strtolower(pathinfo($relativePath, PATHINFO_EXTENSION)) !== 'svg') {
- throw new Exception('Invalid file type: only SVG files are allowed', 400);
- }
+ $this->assertSvgFileName(basename((string) $relativePath));
}
- // Check uploaded file type
+ // Uploads: controller passes the inner $_FILES part (name, tmp_name, type, ...).
+ // Also accept a nested ['file' => [...]] shape if ever used.
if (isset($file['file']) && is_array($file['file'])) {
- $uploadedFile = $file['file'];
-
- // Check file extension
- $fileName = $uploadedFile['name'] ?? '';
- if (strtolower(pathinfo($fileName, PATHINFO_EXTENSION)) !== 'svg') {
- throw new Exception('Invalid file type: only SVG files are allowed', 400);
+ $this->assertUploadedSvg($file['file']);
+ } elseif (isset($file['tmp_name']) || (isset($file['name']) && array_key_exists('error', $file))) {
+ $this->assertUploadedSvg($file);
+ // Normalize stored name after checks
+ if (isset($file['name'])) {
+ $file['name'] = sanitize_file_name((string) $file['name']);
+ $this->assertSvgFileName($file['name']);
}
+ }
+
+ parent::__construct($file);
+ }
- // Check MIME type for additional security
- $mimeType = $uploadedFile['type'] ?? '';
+ /**
+ * Validate an uploaded file array (PHP $_FILES item shape).
+ *
+ * @param array $uploadedFile
+ * @throws Exception
+ */
+ private function assertUploadedSvg(array $uploadedFile): void
+ {
+ $fileName = isset($uploadedFile['name']) ? (string) $uploadedFile['name'] : '';
+ $fileName = str_replace("