Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/kali-forms/Inc/Backend/class-js-vars.php
+++ b/kali-forms/Inc/Backend/class-js-vars.php
@@ -109,11 +109,16 @@
/**
* Grid content
*/
- $this->content['grid'] = json_decode($this->get('grid', '[]'));
+ $this->content['grid'] = Sanitizers::decode_json_meta($this->get('grid', '[]'));
/**
* Field components saved in the database
*/
- $this->content['fieldComponents'] = json_decode($this->get('field_components', '[]'), false, 512, JSON_HEX_QUOT);
+ $this->content['fieldComponents'] = Sanitizers::decode_json_meta(
+ $this->get('field_components', '[]'),
+ false,
+ 512,
+ JSON_HEX_QUOT
+ );
/**
* Form Info Fields
*/
--- a/kali-forms/Inc/Backend/class-sanitizers.php
+++ b/kali-forms/Inc/Backend/class-sanitizers.php
@@ -150,13 +150,256 @@
}
/**
+ * JSON post meta keys that may have been corrupted by legacy slash escaping.
+ *
+ * @var string[]
+ */
+ private static $json_meta_keys = [
+ 'kaliforms_field_components',
+ 'kaliforms_grid',
+ 'kaliforms_emails',
+ 'kaliforms_akismet_fields',
+ 'kaliforms_conditional_thank_you_message',
+ ];
+
+ /**
+ * Prevent recursive meta repair while persisting a fixed value.
+ *
+ * @var array<string, bool>
+ */
+ private static $repairing_meta = [];
+
+ /**
+ * Register runtime hooks for JSON meta repair.
+ *
+ * @return void
+ */
+ public static function register_hooks()
+ {
+ add_filter('get_post_metadata', [__CLASS__, 'filter_repair_json_post_meta'], 10, 4);
+ }
+
+ /**
+ * Repair corrupted JSON post meta on read and persist the cleaned value.
+ *
+ * @param mixed $check Short-circuit return value.
+ * @param int $object_id Post ID.
+ * @param string $meta_key Meta key.
+ * @param bool $single Whether a single value was requested.
+ * @return mixed
+ */
+ public static function filter_repair_json_post_meta($check, $object_id, $meta_key, $single)
+ {
+ if (null !== $check || ! $single) {
+ return $check;
+ }
+
+ if (! in_array($meta_key, self::$json_meta_keys, true)) {
+ return null;
+ }
+
+ if ('kaliforms_forms' !== get_post_type($object_id)) {
+ return null;
+ }
+
+ $cache_key = $object_id . ':' . $meta_key;
+ if (! empty(self::$repairing_meta[$cache_key])) {
+ return null;
+ }
+
+ self::$repairing_meta[$cache_key] = true;
+
+ $raw = get_metadata_raw('post', $object_id, $meta_key, true);
+ if (! is_string($raw) || '' === trim($raw)) {
+ unset(self::$repairing_meta[$cache_key]);
+ return null;
+ }
+
+ $repair = self::repair_json_meta_string($raw);
+
+ if (! $repair['valid']) {
+ unset(self::$repairing_meta[$cache_key]);
+ return null;
+ }
+
+ if ($repair['was_repaired']) {
+ update_post_meta($object_id, $meta_key, $repair['canonical']);
+ $stored = get_metadata_raw('post', $object_id, $meta_key, true);
+ unset(self::$repairing_meta[$cache_key]);
+ return is_string($stored) ? $stored : $repair['canonical'];
+ }
+
+ unset(self::$repairing_meta[$cache_key]);
+ return null;
+ }
+
+ /**
+ * Attempt to recover JSON from over-escaped post meta strings.
+ *
+ * @param string $value Raw meta value.
+ * @return array{valid:bool,was_repaired:bool,canonical:string,decoded:mixed}
+ */
+ public static function repair_json_meta_string($value)
+ {
+ $failure = [
+ 'valid' => false,
+ 'was_repaired' => false,
+ 'canonical' => '[]',
+ 'decoded' => [],
+ ];
+
+ if (! is_string($value) || '' === trim($value)) {
+ return [
+ 'valid' => true,
+ 'was_repaired' => false,
+ 'canonical' => '[]',
+ 'decoded' => [],
+ ];
+ }
+
+ $direct = json_decode($value, true);
+ if (JSON_ERROR_NONE === json_last_error()) {
+ return [
+ 'valid' => true,
+ 'was_repaired' => false,
+ 'canonical' => wp_json_encode($direct),
+ 'decoded' => $direct,
+ ];
+ }
+
+ $attempts = array_unique(
+ [
+ $value,
+ wp_unslash($value),
+ stripslashes($value),
+ wp_unslash(stripslashes($value)),
+ ]
+ );
+
+ foreach ($attempts as $candidate) {
+ $decoded = json_decode($candidate, true);
+ if (JSON_ERROR_NONE === json_last_error()) {
+ return [
+ 'valid' => true,
+ 'was_repaired' => true,
+ 'canonical' => wp_json_encode($decoded),
+ 'decoded' => $decoded,
+ ];
+ }
+ }
+
+ $repaired = self::iteratively_unescape_json_string($value, 'stripslashes');
+ if (null !== $repaired) {
+ return [
+ 'valid' => true,
+ 'was_repaired' => true,
+ 'canonical' => wp_json_encode($repaired),
+ 'decoded' => $repaired,
+ ];
+ }
+
+ $repaired = self::iteratively_unescape_json_string($value, 'wp_unslash');
+ if (null !== $repaired) {
+ return [
+ 'valid' => true,
+ 'was_repaired' => true,
+ 'canonical' => wp_json_encode($repaired),
+ 'decoded' => $repaired,
+ ];
+ }
+
+ $repaired = self::iteratively_unescape_json_string($value, 'collapse_slashes');
+ if (null !== $repaired) {
+ return [
+ 'valid' => true,
+ 'was_repaired' => true,
+ 'canonical' => wp_json_encode($repaired),
+ 'decoded' => $repaired,
+ ];
+ }
+
+ return $failure;
+ }
+
+ /**
+ * Repeatedly unescape a JSON string until it decodes or stops changing.
+ *
+ * @param string $value Raw value.
+ * @param string $method Unescape strategy.
+ * @return array|null
+ */
+ private static function iteratively_unescape_json_string($value, $method)
+ {
+ $candidate = $value;
+
+ for ($i = 0; $i < 25; $i++) {
+ $decoded = json_decode($candidate, true);
+ if (JSON_ERROR_NONE === json_last_error()) {
+ return $decoded;
+ }
+
+ if ('stripslashes' === $method) {
+ $next = stripslashes($candidate);
+ } elseif ('wp_unslash' === $method) {
+ $next = wp_unslash($candidate);
+ } else {
+ $next = str_replace('\\', '\', $candidate);
+ }
+
+ if ($next === $candidate) {
+ break;
+ }
+
+ $candidate = $next;
+ }
+
+ return null;
+ }
+
+ /**
+ * Decode JSON stored in post meta, tolerating legacy slash-escaping.
+ *
+ * @param mixed $value Raw meta value.
+ * @param bool $assoc json_decode associative flag.
+ * @param int $depth json_decode depth.
+ * @param int $flags json_decode flags.
+ * @return mixed
+ */
+ public static function decode_json_meta($value, $assoc = false, $depth = 512, $flags = 0)
+ {
+ if (is_array($value)) {
+ return $value;
+ }
+
+ if (! is_string($value) || '' === trim($value)) {
+ return [];
+ }
+
+ $repair = self::repair_json_meta_string($value);
+ if (! $repair['valid']) {
+ return [];
+ }
+
+ $decoded = json_decode($repair['canonical'], $assoc, $depth, $flags);
+ if (JSON_ERROR_NONE === json_last_error()) {
+ return $decoded;
+ }
+
+ return [];
+ }
+
+ /**
* @param $input
*
* @return false|string
*/
public static function sanitize_grid_layout($input)
{
- $input = json_decode(stripslashes($input));
+ $input = self::decode_json_meta($input);
+ if (! is_array($input)) {
+ return wp_json_encode([]);
+ }
+
$sanitized = [];
foreach ($input as $item) {
$grid = Sanitizers::sanitize_grid_item($item);
@@ -207,8 +450,8 @@
*/
public static function sanitize_field_components($input)
{
- $input = json_decode(stripslashes($input));
- if (null === $input) {
+ $input = self::decode_json_meta($input);
+ if (! is_array($input)) {
return wp_json_encode([]);
}
@@ -217,7 +460,7 @@
$sanitized[] = Sanitizers::sanitize_field_component($field);
}
- return wp_slash(wp_json_encode($sanitized, JSON_HEX_QUOT));
+ return wp_json_encode($sanitized, JSON_HEX_QUOT);
}
/**
@@ -232,7 +475,7 @@
$fieldItem->internalId = sanitize_key($item->internalId);
$fieldItem->label = sanitize_text_field($item->label);
$fieldItem->properties = Sanitizers::sanitize_properties_object($item->properties, $item->id);
- $fieldItem->constraint = empty($item->constraint) ? 'none' : absint($item->constraint);
+ $fieldItem->constraint = (empty($item->constraint) || 'none' === $item->constraint) ? 'none' : absint($item->constraint);
return $fieldItem;
}
@@ -421,7 +664,7 @@
}
}
- return wp_slash(wp_json_encode($sanitized, JSON_HEX_QUOT));
+ return wp_json_encode($sanitized, JSON_HEX_QUOT);
}
/**
--- a/kali-forms/Inc/class-kaliforms.php
+++ b/kali-forms/Inc/class-kaliforms.php
@@ -16,6 +16,7 @@
use KaliFormsIncBackendPlugin_Review;
use KaliFormsIncBackendEntries_Deleter;
use KaliFormsIncBackendLicense_Checker;
+use KaliFormsIncBackendSanitizers;
use KaliFormsIncBackendPostsForms;
use KaliFormsIncBackendPostsSubmitted;
use KaliFormsIncBackendPredefined_Forms;
@@ -82,6 +83,7 @@
* Create an instance of the meta save
*/
Meta_Save::get_instance();
+ Sanitizers::register_hooks();
/**
* Register the new custom post types
*/
--- a/kali-forms/bootstrap.php
+++ b/kali-forms/bootstrap.php
@@ -15,4 +15,4 @@
define('KALIFORMS_BASE_API', 'https://www.kaliforms.com/wp-json/wp/v2/');
define('KALIFORMS_EXTENSIONS_API', 'https://kaliforms.com/wp-json/kf/v1/plugins');
define('KALIFORMS_UNINSTALL_FEEDBACK_API', 'https://kaliforms.com/wp-json/kf/v1/uninstall-feedback');
-define('KALIFORMS_VERSION', '2.4.13');
+define('KALIFORMS_VERSION', '2.4.14');
--- a/kali-forms/kali-forms.php
+++ b/kali-forms/kali-forms.php
@@ -5,7 +5,7 @@
* Plugin URI: https://www.kaliforms.com
* Description: Kali Forms provides a user-friendly form creation experience for WordPress.
* Author: Kali Forms
- * Version: 2.4.13
+ * Version: 2.4.14
* Author URI: https://www.kaliforms.com/
* License: GPLv3 or later
* License URI: http://www.gnu.org/licenses/gpl-3.0.html