Published : August 6, 2026

CVE-2026-11881: Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder < 6.2.6 Authenticated (Contributor+) Stored Cross-Site Scripting PoC, Patch Analysis & Rule

Plugin fluentform
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 6.2.6
Patched Version 6.2.6
Disclosed July 29, 2026

Analysis Overview

“`json
{
“analysis”: “Atomic Edge analysis of CVE-2026-11881: Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder < 6.2.6 – Authenticated (Contributor+) Stored Cross-Site Scripting. This vulnerability allows authenticated attackers with contributor-level access to inject and store arbitrary web scripts that execute whenever a user views the affected page. The issue lies in insufficient input sanitization and output escaping, with a CVSS score of 6.4.",

"Root Cause: The patch primarily focuses on a privilege escalation fix in the Acl.php file, where the 'hasPermission' method was updated to skip the role fallback for explicit managers, preventing a limited manager from escalating privileges via a delegated role. While this is a distinct security fix, the provided diff for CVE-2026-11881 shows code hardening and refactoring across Helper.php and PaymentHandler.php. The core vulnerability stems from a lack of proper 'sanitize_text_field()' and 'esc_html()' calls on form field values, labels, and options. The diff shows a new helper, 'getOtherOptionValuePrefix', introduced to handle 'Other' option values in form submissions, and 'SubmissionHandlerService.php' now uses this helper to prefix user-supplied 'Other' values. The previous code used a static 'Other: ' prefix. This change, while improving label handling, does not apply the necessary escaping to the user-controlled input, leaving the stored XSS vector open.",

"Exploitation: An authenticated attacker with a contributor role can create or edit a form. The attacker can inject a malicious script into a form field's label, options, or the 'Other' option value. When a form entry is submitted with a crafted 'Other' value containing a script payload (e.g., 'Other: alert(document.cookie)’), the ‘SubmissionHandlerService::prepareHandler’ method processes it. The input value is sanitized with ‘sanitize_text_field()’ in the submission handling, which strips some characters but does not prevent XSS because it does not use ‘esc_html()’ for output., The stored value is then rendered back to an administrator or other user viewing the form’s entries via the ‘fluentform_entry_details’ page, executing the script. This allows the attacker to steal cookies, session tokens, or perform actions on behalf of the victim.”,

“Patch Analysis: The patch introduces changes in ‘Acl.php’ to address a separate privilege escalation issue. For the XSS vulnerability, ‘Helper.php’ now includes ‘getOtherOptionValuePrefix()’, a function to use the field’s translated label as a prefix for the ‘Other’ value. ‘SubmissionHandlerService.php’ has been updated to use this new function. Before the patch, the code used a static ‘Other: ‘ prefix and did not account for the specific field’s label. The patch changes the prefix that is prepended to the user-supplied input but does not add output escaping (e.g., ‘esc_html()’) to the final rendered value. The change increments the default value of ‘save_usage’ in ‘AiFormBuilder.php’ but no security measures are added to prevent the stored XSS.”,

“Impact: The successful exploitation of this vulnerability leads to stored cross-site scripting. An attacker can inject malicious JavaScript that executes in the context of an authenticated administrator’s browser session. This can lead to full account compromise, session hijacking, defacement of the WordPress site, or the injection of malware. The attacker could potentially create new admin accounts, modify form settings, and exfiltrate sensitive data.”
}
“`

Differential between vulnerable and patched code

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

Code Diff
--- a/fluentform/app/Helpers/Helper.php
+++ b/fluentform/app/Helpers/Helper.php
@@ -206,7 +206,7 @@
             'fluentform_entry_statuses_core',
             [
                 $statuses,
-                $form_id
+                $form_id,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/entry_statuses_core',
@@ -235,7 +235,7 @@
         $data = apply_filters_deprecated(
             'fluentform_reportable_inputs',
             [
-                $data
+                $data,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/reportable_inputs',
@@ -250,7 +250,7 @@
         $grid = apply_filters_deprecated(
             'fluentform_subfield_reportable_inputs',
             [
-                ['tabular_grid']
+                ['tabular_grid'],
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/subfield_reportable_inputs',
@@ -263,7 +263,7 @@
     public static function getFormMeta($formId, $metaKey, $default = '', $forced = false)
     {
         $formattedValues = self::$formMetaCache[$formId] ?? [];
-
+
         if (!isset(self::$formMetaCache[$formId]) || $forced) {
             $formMetas = FormMeta::where('form_id', $formId)
                 ->get();
@@ -271,7 +271,7 @@
             $formattedValues = [];
             foreach ($formMetas as $formMeta) {
                 $value = $formMeta->value;
-
+
                 $decoded = json_decode($value ?? '', true);
                 if (is_array($decoded)) {
                     $value = $decoded;
@@ -391,7 +391,7 @@

     public static function getFormInstaceClass($formId)
     {
-        static::$formInstance += 1;
+        static::$formInstance++;

         return 'ff_form_instance_' . $formId . '_' . static::$formInstance;
     }
@@ -411,7 +411,7 @@
             'fluent_forms_add_ons',
             'fluent_forms_docs',
             'fluent_forms_payment_entries',
-            'fluent_forms_reports'
+            'fluent_forms_reports',
         ];

         $status = true;
@@ -425,7 +425,7 @@
         $status = apply_filters_deprecated(
             'fluentform_is_admin_page',
             [
-                $status
+                $status,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/is_admin_page',
@@ -457,7 +457,7 @@
                 $result = shortcode_parse_atts($parsedCode);

                 if (!empty($result[$selector])) {
-                    if ($tag == 'fluentform' && !empty($result['type']) && $result['type'] == 'conversational') {
+                    if ('fluentform' == $tag && !empty($result['type']) && 'conversational' == $result['type']) {
                         continue;
                     }

@@ -468,7 +468,7 @@
                     if ($theme) {
                         $attributes[] = [
                             'formId' => $result[$selector],
-                            'theme'  => $theme
+                            'theme'  => $theme,
                         ];
                     }
                 }
@@ -491,7 +491,7 @@
             return $ids;
         }

-        $has_block = false !== strpos($content, '<!-- wp:fluentfom/guten-block' . ' ');
+        $has_block = false !== strpos($content, '<!-- wp:fluentfom/guten-block ');

         if (!$has_block) {
             return $ids;
@@ -505,7 +505,7 @@

             $hasBlock = strpos($block['blockName'], 'fluentfom/guten-block') === 0;
             if ($hasBlock) {
-                $formId = (int)$block['attrs']['formId'];
+                $formId = (int) $block['attrs']['formId'];

                 $ids[] = $formId;

@@ -514,7 +514,7 @@
                 if ($theme) {
                     $attributes[] = [
                         'formId' => $formId,
-                        'theme'  => $theme
+                        'theme'  => $theme,
                     ];
                 }
             }
@@ -550,8 +550,8 @@
             return false;
         }

-        $fieldsJson = (string)($form->form_fields ?? '');
-        if ($fieldsJson === '') {
+        $fieldsJson = (string) ($form->form_fields ?? '');
+        if ('' === $fieldsJson) {
             return false;
         }

@@ -560,7 +560,7 @@
             return false;
         }

-        return (bool)ArrayHelper::get($fields, 'stepsWrapper');
+        return (bool) ArrayHelper::get($fields, 'stepsWrapper');
     }

     public static function hasFormElement($formId, $elementName)
@@ -584,8 +584,8 @@
                 // if form has pending payment then the value doesn't exist in EntryDetails table
                 // further checking on Submission table if the value exists
                 if (!$exist && $form->has_payment) {
-                    $escapedKey = json_encode($fieldName);
-                    $escapedValue = json_encode($inputValue);
+                    $escapedKey = wp_json_encode($fieldName);
+                    $escapedValue = wp_json_encode($inputValue);
                     $searchPattern = trim($escapedKey, '"') . '":' . $escapedValue;
                     $searchPattern = addcslashes($searchPattern, '%_');

@@ -672,7 +672,7 @@
         $data = apply_filters_deprecated(
             'fluentform_numeric_styles',
             [
-                $data
+                $data,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/numeric_styles',
@@ -753,10 +753,8 @@
                     $columnInputs = static::getFieldNamesStatuses(ArrayHelper::get($column, 'fields', []));
                     $names = array_merge($names, $columnInputs);
                 }
-            } else {
-                if ($name = ArrayHelper::get($field, 'attributes.name')) {
+            } elseif ($name = ArrayHelper::get($field, 'attributes.name')) {
                     $names[] = $name;
-                }
             }
         }

@@ -807,14 +805,18 @@
                 'sanitize_text_field',
                 array_column(static::flattenAdvancedOptions($formattedOptions), 'value')
             ), function ($value) {
-                return $value !== '';
+                return '' !== $value;
             }));

             if (count($optionValues) !== count(array_unique($optionValues))) {
-                $duplicates[] = ArrayHelper::get($field, 'settings.admin_field_label')
-                    ?: ArrayHelper::get($field, 'settings.label')
-                    ?: ArrayHelper::get($field, 'attributes.name')
-                    ?: __('Ranking Field', 'fluentform');
+                $fieldLabel = ArrayHelper::get($field, 'settings.admin_field_label');
+                if (!$fieldLabel) {
+                    $fieldLabel = ArrayHelper::get($field, 'settings.label');
+                }
+                if (!$fieldLabel) {
+                    $fieldLabel = ArrayHelper::get($field, 'attributes.name');
+                }
+                $duplicates[] = $fieldLabel ? $fieldLabel : __('Ranking Field', 'fluentform');
             }
         }

@@ -839,10 +841,8 @@
             return static::getConversionUrl($formId);
         } elseif ('classic' == $type) {
             return site_url('?fluent_forms_pages=1&design_mode=1&preview_id=' . $formId) . '#ff_preview';
-        } else {
-            if (static::isConversionForm($formId)) {
+        } elseif (static::isConversionForm($formId)) {
                 return static::getConversionUrl($formId);
-            }
         }

         return site_url('?fluent_forms_pages=1&design_mode=1&preview_id=' . $formId) . '#ff_preview';
@@ -870,7 +870,7 @@
         $slug = apply_filters_deprecated(
             'fluentform_conversational_url_slug',
             [
-                'fluent-form'
+                'fluent-form',
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/conversational_url_slug',
@@ -904,7 +904,7 @@
         $locations = apply_filters_deprecated(
             'fluentform_file_upload_options',
             [
-                $locations
+                $locations,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/file_upload_options',
@@ -989,7 +989,7 @@
             'fluentform_truncate_password_values',
             [
                 true,
-                $formId
+                $formId,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/truncate_password_values',
@@ -1011,8 +1011,7 @@
         $rowJoiner = '<br />',
         $colJoiner = ', ',
         $type = ''
-    )
-    {
+    ) {
         if (!$girdData || !$field) {
             return '';
         }
@@ -1039,7 +1038,7 @@
                     if ($girdCols && isset($girdCols[$item])) {
                         $item = $girdCols[$item];
                     }
-                    if ($index == (count($column) - 1)) {
+                    if ((count($column) - 1) == $index) {
                         $_colJoiner = '';
                     }
                     $value .= $item . $_colJoiner;
@@ -1116,7 +1115,7 @@
     public static function isAutosaveEnabled()
     {
         $autosaveEnabled = ArrayHelper::get(get_option('_fluentform_global_form_settings'), 'misc.autosave_enabled', 'no');
-        return $autosaveEnabled === 'yes';
+        return 'yes' === $autosaveEnabled;
     }

     public static function maybeDecryptUrl($url)
@@ -1144,7 +1143,7 @@
     public static function isBlockEditor()
     {
         // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking REST API context
-        return defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && $_REQUEST['context'] === 'edit';
+        return defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && 'edit' === $_REQUEST['context'];
     }

     public static function resolveValidationRulesGlobalOption(&$field)
@@ -1153,14 +1152,12 @@
             foreach ($field['fields'] as &$subField) {
                 static::resolveValidationRulesGlobalOption($subField);
             }
-        } else {
-            if (ArrayHelper::get($field, 'settings.validation_rules')) {
-                foreach ($field['settings']['validation_rules'] as $key => &$rule) {
-                    if (!isset($rule['global'])) {
-                        $rule['global'] = false;
-                    }
-                    $rule['global_message'] = static::getGlobalDefaultMessage($key);
+        } elseif (ArrayHelper::get($field, 'settings.validation_rules')) {
+            foreach ($field['settings']['validation_rules'] as $key => &$rule) {
+                if (!isset($rule['global'])) {
+                    $rule['global'] = false;
                 }
+                $rule['global_message'] = static::getGlobalDefaultMessage($key);
             }
         }
     }
@@ -1197,7 +1194,8 @@
             $fieldType = ArrayHelper::get($rawField, 'element');
             $rawField = apply_filters('fluentform/rendering_field_data_' . $fieldType, $rawField, $form);
             $options = [];
-            if ("net_promoter_score" === $fieldType) {
+            $otherPrefix = '';
+            if ('net_promoter_score' === $fieldType) {
                 $options = array_flip(ArrayHelper::get($rawField, 'options', []));
             } elseif ('ratings' == $fieldType) {
                 $options = array_keys(ArrayHelper::get($rawField, 'options', []));
@@ -1222,14 +1220,15 @@
                 }

                 $options = array_column(self::flattenAdvancedOptions($formattedOptions), 'value');
-
+
                 // Add field-specific __ff_other__ to options if "Other" option is enabled
                 if (in_array($fieldType, ['input_checkbox', 'input_radio']) &&
                     ArrayHelper::get($rawField, 'settings.enable_other_option') === 'yes') {
                     $fieldName = sanitize_key(str_replace(['[', ']'], '', ArrayHelper::get($rawField, 'attributes.name', '')));
                     $options[] = '__ff_other_' . $fieldName . '__';
+                    $otherPrefix = static::getOtherOptionValuePrefix($rawField);
                 }
-            } elseif ("dynamic_field" == $fieldType) {
+            } elseif ('dynamic_field' == $fieldType) {
                 $dynamicFetchValue = 'yes' == ArrayHelper::get($rawField, 'settings.dynamic_fetch');
                 if ($dynamicFetchValue) {
                     $rawField = apply_filters('fluentform/dynamic_field_re_fetch_result_and_resolve_value', $rawField);
@@ -1262,11 +1261,11 @@
                     }

                     $filteredValues = array_values(array_filter(array_map('sanitize_text_field', $inputValue), function ($value) {
-                        return $value !== '';
+                        return '' !== $value;
                     }));

                     $normalizedOptions = array_values(array_filter(array_map('sanitize_text_field', $options), function ($value) {
-                        return $value !== '';
+                        return '' !== $value;
                     }));

                     sort($filteredValues);
@@ -1285,28 +1284,26 @@
                 case 'input_checkbox':
                 case 'multi_select':
                 case 'dynamic_field_options':
-
                     $skipValidationInputsWithOptions = apply_filters('fluentform/skip_validation_inputs_with_options', false, $fieldType, $form, $formData);
                     if ($skipValidationInputsWithOptions) {
                         break;
                     }
                     if (is_array($inputValue)) {
-                        // Handle field-specific "Other" options for checkboxes
-                        $filteredValues = array_filter($inputValue, function($value) {
-                            // Skip field-specific other values and processed other values
+                        // Skip "Other" values — raw, localized or legacy English prefix
+                        $filteredValues = array_filter($inputValue, function ($value) use ($otherPrefix) {
                             return !preg_match('/^__ff_other_.*__$/', $value) &&
-                                   !preg_match('/^Other:s/', $value);
+                                    !preg_match('/^Other:s/', $value) &&
+                                    !($otherPrefix && 0 === strpos($value, $otherPrefix));
                         });
                         $isValid = array_diff($filteredValues, $options);
                         $isValid = empty($isValid);
+                    } elseif (preg_match('/^__ff_other_.*__$/', $inputValue) ||
+                        preg_match('/^Other:s/', $inputValue) ||
+                        ($otherPrefix && 0 === strpos($inputValue, $otherPrefix))) {
+                        // Accept "Other" values — raw, localized or legacy English prefix
+                        $isValid = true;
                     } else {
-                        // Handle field-specific "Other" option for single values
-                        if (preg_match('/^__ff_other_.*__$/', $inputValue) ||
-                            preg_match('/^Other:s/', $inputValue)) {
-                            $isValid = true;
-                        } else {
-                            $isValid = in_array($inputValue, $options);
-                        }
+                        $isValid = in_array($inputValue, $options);
                     }
                     break;
                 case 'input_number':
@@ -1370,6 +1367,32 @@
         return $error;
     }

+    /**
+     * Prefix used to store a checkable field's "Other" option value,
+     * built from the field's own (translated) label. Pass $form to run
+     * the field through the rendering filter (translation plugins) first.
+     *
+     * @param array $rawField
+     * @param object|null $form
+     * @return string
+     */
+    public static function getOtherOptionValuePrefix($rawField, $form = null)
+    {
+        $fieldType = ArrayHelper::get($rawField, 'element');
+        if ($form && $fieldType) {
+            $rawField = apply_filters('fluentform/rendering_field_data_' . $fieldType, $rawField, $form);
+        }
+
+        $label = trim((string) ArrayHelper::get($rawField, 'settings.other_option_label'));
+
+        if ('' === $label) {
+            $label = __('Other', 'fluentform');
+        }
+
+        // Avoid "::" when the label already ends with a colon
+        return ':' === substr($label, -1) ? $label . ' ' : $label . ': ';
+    }
+
     public static function getWhiteListedFields($formId)
     {
         $whiteListedFields = [
@@ -1385,7 +1408,7 @@
             '__square_payment_method_id',
             '__square_verify_buyer_id',
             'ct_bot_detector_event_token',
-            'ff_ct_form_load_time'
+            'ff_ct_form_load_time',
         ];

         return apply_filters('fluentform/white_listed_fields', $whiteListedFields, $formId);
@@ -1393,6 +1416,7 @@

     /**
      * Shortcode parse on validation message
+     *
      * @param string $message
      * @param object $form
      * @param string $fieldName
@@ -1401,7 +1425,7 @@
     public static function shortCodeParseOnValidationMessage($message, $form, $fieldName)
     {
         // Return early if form is null to prevent errors
-        if ($form === null) {
+        if (null === $form) {
             return $message;
         }

@@ -1409,7 +1433,7 @@
         // Add 'current_field' name as data array to resolve {labels.current_field} shortcode if it has
         return ShortCodeParser::parse(
             $message,
-            (object)['response' => "", 'form_id' => $form->id],
+            (object) ['response' => '', 'form_id' => $form->id],
             ['current_field' => $fieldName],
             $form
         );
@@ -1565,7 +1589,7 @@
             'HTTP_X_COUNTRY',
             'X-Country',
             'HTTP_X_COUNTRY_ISO',
-            'X-Country-ISO'
+            'X-Country-ISO',
         ];

         foreach ($headers as $header) {
@@ -1573,8 +1597,8 @@
             if (isset($_SERVER[$header])) {
                 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Country code from CDN/proxy header, validated below
                 $code = trim(sanitize_text_field(wp_unslash($_SERVER[$header])));
-            } // Try with HTTP_ prefix if not already present
-            elseif (strpos($header, 'HTTP_') !== 0) {
+            } elseif (strpos($header, 'HTTP_') !== 0) {
+                // Try with HTTP_ prefix if not already present
                 $httpHeader = 'HTTP_' . str_replace('-', '_', strtoupper($header));
                 if (isset($_SERVER[$httpHeader])) {
                     // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Country code from CDN/proxy header, validated below
@@ -1587,7 +1611,7 @@
             }

             // Basic validation - should be 2-letter country code
-            if (!empty($code) && is_string($code) && strlen($code) === 2 && ctype_alpha($code) && $code !== 'XX') {
+            if (!empty($code) && is_string($code) && 2 === strlen($code) && ctype_alpha($code) && 'XX' !== $code) {
                 return strtoupper($code);
             }
         }
@@ -1597,6 +1621,7 @@

     /**
      * Fixes PHP Object Injection Vulnerability
+     *
      * @param $data
      * @return mixed
      */
@@ -1608,10 +1633,11 @@
         return $data;
     }

-	/**
-	 * If elementor editor is open
-	 * @return bool
-	 */
+    /**
+     * If elementor editor is open
+     *
+     * @return bool
+     */
     public static function isElementorEditor()
     {
         return defined('ELEMENTOR_VERSION') &&
@@ -1623,6 +1649,7 @@
     /**
      * Check if we're in block editor context (Site Editor, Template Editor, or Post/Page Editor)
      * Covers all Gutenberg block editor contexts including mobile/tablet preview iframes
+     *
      * @return bool
      */
     public static function isSiteEditor()
@@ -1653,8 +1680,8 @@
         $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
         // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking block editor context
         return isset( $_GET['_wp-find-template'] ) ||
-               strpos( $request_uri, 'site-editor.php' ) !== false ||
+                strpos( $request_uri, 'site-editor.php' ) !== false ||
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking REST API context
-               (defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && $_REQUEST['context'] === 'edit');
+                (defined('REST_REQUEST') && REST_REQUEST && !empty($_REQUEST['context']) && 'edit' === $_REQUEST['context']);
     }
 }
--- a/fluentform/app/Modules/Acl/Acl.php
+++ b/fluentform/app/Modules/Acl/Acl.php
@@ -13,7 +13,7 @@

     public static function normalizeFormId($formId)
     {
-        if ($formId === null || $formId === false || $formId === '') {
+        if (null === $formId || false === $formId || '' === $formId) {
             return null;
         }

@@ -62,11 +62,11 @@
             'fluentform_settings_manager',
             'fluentform_full_access',
         ];
-
+
         $data = apply_filters_deprecated(
             'fluentform_permission_set',
             [
-                $data
+                $data,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/permission_set',
@@ -165,7 +165,8 @@
             return true;
         }

-        $grantedRole = static::getCurrentUserCapability();
+        // Skip the role fallback for explicit managers, else a limited manager escalates.
+        $grantedRole = self::isExplicitManager() ? false : static::getCurrentUserCapability();

         foreach ((array) $permissions as $permission) {
             $allowed = current_user_can($permission);
@@ -190,13 +191,40 @@
         return current_user_can('fluentform_full_access') || current_user_can('manage_options');
     }

+    // Is the CURRENT user a Manager added by name (per-user), not just someone riding a delegated role?
+    private static function isExplicitManager()
+    {
+        $userId = get_current_user_id();
+
+        return (bool) ($userId && self::userHasDirectGrant($userId, wp_get_current_user()));
+    }
+
+    // "Direct grant" = permissions attached to the USER themselves (per-user Manager),
+    // as opposed to access inherited from a delegated WordPress role.
+    private static function userHasDirectGrant($userId, $user)
+    {
+        // Flag set when an admin adds the user via Settings -> Managers.
+        if (get_user_meta($userId, '_fluent_forms_has_role', true)) {
+            return true;
+        }
+
+        // Legacy fallback: caps stored on the user itself ($user->caps), not merged in from a role.
+        foreach (static::getPermissionSet() as $permission) {
+            if ($user && !empty($user->caps[$permission])) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     private static function filterPermissionCheck($permission, $allowed, $formId)
     {
         $allowed = apply_filters_deprecated(
             'fluentform_verify_user_permission_' . $permission,
             [
                 $allowed,
-                $formId
+                $formId,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/verify_user_permission_' . $permission,
@@ -351,16 +379,20 @@
         $isSuperMan = static::isSuperMan($user);
         $capability = static::findUserCapability($user);

-        if ($isSuperMan || $capability) {
-            if ($isSuperMan) {
-                // $permissionSet[] = 'administrator';
-            }
+        $isManager = self::userHasDirectGrant($user->ID, $user);

+        if ($isSuperMan) {
             return $permissionSet;
         }

         $userPermissions = array_values(array_intersect(array_keys($user->allcaps), $permissionSet));

+        // Delegated-role users still return before the filter (unchanged boundary);
+        // a manager just reports their own scoped caps instead of the full set.
+        if ($capability) {
+            return $isManager ? $userPermissions : $permissionSet;
+        }
+
         return apply_filters('fluentform/current_user_permissions', $userPermissions);
     }

--- a/fluentform/app/Modules/Ai/AiFormBuilder.php
+++ b/fluentform/app/Modules/Ai/AiFormBuilder.php
@@ -109,7 +109,8 @@
             'site_locale'    => determine_locale(),
             'has_pro'        => Helper::hasPro(),
             'has_payment'    => $paymentSetting['status'] == 'yes',
-            'request_id'     => uniqid('ff_ai_')
+            'request_id'     => uniqid('ff_ai_'),
+            'save_usage'     => apply_filters('fluentform/ai_save_usage', true),
         ];

         $result = (new FluentFormAIAPI())->makeRequest($queryArgs);
--- a/fluentform/app/Modules/Payments/PaymentHandler.php
+++ b/fluentform/app/Modules/Payments/PaymentHandler.php
@@ -396,14 +396,18 @@
     {
         // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Nonce verified by Acl::verify()
         $route = isset($_REQUEST['route']) ? sanitize_text_field(wp_unslash($_REQUEST['route'])) : '';
-        $formScopedRoutes = [
-            'get_form_settings',
-            'save_form_settings',
+        $paymentMutationRoutes = [
             'update_transaction',
             'cancel_subscription'
         ];
+        $formSettingRoutes = [
+            'get_form_settings',
+            'save_form_settings'
+        ];

-        if (in_array($route, $formScopedRoutes, true)) {
+        if (in_array($route, $paymentMutationRoutes, true)) {
+            Acl::verify('fluentform_manage_payments', $this->resolveRouteFormId($route));
+        } elseif (in_array($route, $formSettingRoutes, true)) {
             Acl::verify('fluentform_forms_manager', $this->resolveRouteFormId($route));
         } else {
             Acl::verify('fluentform_settings_manager');
--- a/fluentform/app/Services/Form/SubmissionHandlerService.php
+++ b/fluentform/app/Services/Form/SubmissionHandlerService.php
@@ -35,6 +35,7 @@

     /**
      * Form Submission
+     *
      * @param $formDataRaw
      * @param $formId
      * @return array
@@ -53,6 +54,8 @@
     }

     /**
+     * Prepare the form and validated form data for submission handling.
+     *
      * @throws ValidationException
      */
     protected function prepareHandler($formId, $formDataRaw)
@@ -72,13 +75,16 @@
         foreach ($formDataRaw as $name => $input) {
             if (is_array($input)) {
                 $formDataRaw[$name] = array_filter($input, function ($value) {
-                    return $value !== null && $value !== false && $value !== '';
+                    return null !== $value && false !== $value && '' !== $value;
                 });
             }

             // Process "Other" options for checkboxes and radio fields
             if (strpos($name, '__ff_other_input__') !== false && !empty($input)) {
                 $fieldName = str_replace('__ff_other_input__', '', $name);
+                // Store with the field's own (translated) "Other" label as prefix
+                $rawField = Arr::get(FormFieldsParser::getInputs($this->form, ['raw']), $fieldName . '.raw', []);
+                $otherPrefix = Helper::getOtherOptionValuePrefix($rawField, $this->form);

                 // Handle checkbox fields (array values)
                 if (isset($formDataRaw[$fieldName]) && is_array($formDataRaw[$fieldName])) {
@@ -89,13 +95,13 @@

                     $key = array_search($otherValue, $selectedValues);

-                    if ($key !== false) {
-                        $selectedValues[$key] = 'Other: ' . sanitize_text_field($input);
+                    if (false !== $key) {
+                        $selectedValues[$key] = $otherPrefix . sanitize_text_field($input);
                         $formDataRaw[$fieldName] = $selectedValues;
                     }
-                } // Handle radio fields (single value)
-                elseif (isset($formDataRaw[$fieldName]) && $formDataRaw[$fieldName] === '__ff_other_' . $fieldName . '__') {
-                    $formDataRaw[$fieldName] = 'Other: ' . sanitize_text_field($input);
+                } elseif (isset($formDataRaw[$fieldName]) && '__ff_other_' . $fieldName . '__' === $formDataRaw[$fieldName]) {
+                    // Handle radio fields (single value)
+                    $formDataRaw[$fieldName] = $otherPrefix . sanitize_text_field($input);
                 }

                 unset($formDataRaw[$name]);
@@ -124,6 +130,7 @@

     /**
      * Prepare the data to be inserted to the database.
+     *
      * @param boolean $formData
      * @return array
      */
@@ -146,7 +153,7 @@
             [
                 $formData,
                 $formId,
-                $inputConfigs
+                $inputConfigs,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/insert_response_data',
@@ -160,7 +167,7 @@
             'fluentform_disable_ip_logging',
             [
                 false,
-                $formId
+                $formId,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/disable_ip_logging',
@@ -175,7 +182,7 @@
         $response = [
             'form_id'       => $formId,
             'serial_number' => $serialNumber,
-            'response'      => json_encode($this->formData, JSON_UNESCAPED_UNICODE),
+            'response'      => wp_json_encode($this->formData, JSON_UNESCAPED_UNICODE),
             'source_url'    => site_url(Arr::get($formData, '_wp_http_referer')),
             'user_id'       => get_current_user_id(),
             'browser'       => $browser->getBrowser(),
@@ -189,7 +196,7 @@
         $response = apply_filters_deprecated(
             'fluentform_filter_insert_data',
             [
-                $response
+                $response,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/filter_insert_data',
@@ -205,10 +212,10 @@
         $formData = isset($this->formData) ? $this->formData : $formData;
         do_action_deprecated(
             'fluentform_before_form_actions_processing', [
-            $insertId,
-            $this->formData,
-            $form
-        ],
+                $insertId,
+                $this->formData,
+                $form,
+            ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/before_form_actions_processing',
             'Use fluentform/before_form_actions_processing instead of fluentform_before_form_actions_processing.'
@@ -237,10 +244,10 @@

             do_action_deprecated(
                 'fluentform_submission_inserted_' . $form->type . '_form', [
-                $insertId,
-                $formData,
-                $form
-            ],
+                    $insertId,
+                    $formData,
+                    $form,
+                ],
                 FLUENTFORM_FRAMEWORK_UPGRADE,
                 'fluentform/submission_inserted_' . $form->type . '_form',
                 'Use fluentform/submission_inserted_' . $form->type . '_form instead of fluentform_submission_inserted_' . $form->type . '_form'
@@ -261,10 +268,10 @@

         do_action_deprecated(
             'fluentform_before_submission_confirmation', [
-            $insertId,
-            $formData,
-            $form
-        ],
+                $insertId,
+                $formData,
+                $form,
+            ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/before_submission_confirmation',
             'Use fluentform/before_submission_confirmation instead of fluentform_before_submission_confirmation.'
@@ -281,6 +288,7 @@

     /**
      * Return Formatted Response Data
+     *
      * @param $insertId
      * @param $form
      * @param $formData
@@ -298,7 +306,7 @@
             [
                 $confirmation,
                 $formData,
-                $form
+                $form,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/form_submission_confirmation',
@@ -321,7 +329,7 @@
                     $confirmation['messageToShow'],
                     $insertId,
                     $formData,
-                    $form
+                    $form,
                 ],
                 FLUENTFORM_FRAMEWORK_UPGRADE,
                 'fluentform/submission_message_parse',
@@ -361,7 +369,7 @@
             }
             $parseUrl = apply_filters_deprecated('fluentform_will_parse_url_value', [
                 true,
-                $form
+                $form,
             ],
                 FLUENTFORM_FRAMEWORK_UPGRADE,
                 'fluentform/will_parse_url_value',
@@ -431,7 +439,7 @@
             $form,
             $confirmation,
             $insertId,
-            $formData
+            $formData,
         ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/submission_confirmation',
@@ -512,6 +520,7 @@

     /**
      * Validates Submission
+     *
      * @throws ValidationException
      */
     private function handleValidation()
@@ -559,7 +568,7 @@
             [
                 $insertData,
                 $formDataRaw,
-                $this->form
+                $this->form,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/before_insert_submission',
@@ -574,7 +583,7 @@
                 [
                     $insertData,
                     $formDataRaw,
-                    $this->form
+                    $this->form,
                 ],
                 FLUENTFORM_FRAMEWORK_UPGRADE,
                 'fluentform/before_insert_payment_form',
@@ -616,8 +625,7 @@
             'component'        => $type . ' Integration',
             'status'           => 'info',
             'title'            => __('Skip Submission Processing', 'fluentform'),
-            'description'      => __('Submission marked as spammed. And skip all actions processing', 'fluentform')
+            'description'      => __('Submission marked as spammed. And skip all actions processing', 'fluentform'),
         ]);
     }
 }
-
--- a/fluentform/app/Services/Form/Updater.php
+++ b/fluentform/app/Services/Form/Updater.php
@@ -14,11 +14,11 @@
 {
     public function update($attributes = [])
     {
-        $formId = (int)Arr::get($attributes, 'form_id');
+        $formId = (int) Arr::get($attributes, 'form_id');
         $formFields = Arr::get($attributes, 'formFields');
         $status = sanitize_text_field(Arr::get($attributes, 'status', 'published'));
         $title = sanitize_text_field(Arr::get($attributes, 'title'));
-
+
         $this->validate([
             'title'      => $title,
             'formFields' => $formFields,
@@ -41,7 +41,7 @@
                 'fluentform_form_fields_update',
                 [
                     $formFields,
-                    $formId
+                    $formId,
                 ],
                 FLUENTFORM_FRAMEWORK_UPGRADE,
                 'fluentform/form_fields_update',
@@ -52,10 +52,11 @@
             $data['form_fields'] = $formFields;
             /**
              * Fires before a Form is updated.
+             *
              * @since 5.2.1
              */
             do_action('fluentform/before_updating_form', $form, $data);
-
+
             $form->fill($data);

             if (FormFieldsParser::hasPaymentFields($form)) {
@@ -65,7 +66,7 @@
             }

             $this->updatePrimaryEmail($form);
-
+
         }

         $form->fill($data)->save();
@@ -93,7 +94,8 @@

                 throw new Exception(
                     sprintf(
-                        __('Ranking field %s has duplicate option values. Please make each option value unique.', 'fluentform'),
+                        // translators: %s is the ranking field name(s) with duplicate option values.
+                        esc_html__('Ranking field %s has duplicate option values. Please make each option value unique.', 'fluentform'),
                         esc_html($duplicateRankingFields)
                     )
                 );
@@ -110,7 +112,7 @@
         if (fluentformCanUnfilteredHTML()) {
             return $formFields;
         }
-
+
         $fieldsArray = json_decode($formFields, true);

         if (isset($fieldsArray['submitButton'])) {
@@ -148,7 +150,7 @@
         ];

         $attributesKeys = array_keys($attributesMap);
-
+
         $settingsMap = [
             'container_class'           => 'sanitize_text_field',
             'label'                     => 'fluentform_sanitize_html',
@@ -169,6 +171,7 @@
             'description'               => 'fluentform_sanitize_html',
             'grid_columns'              => [Helper::class, 'sanitizeArrayKeysAndValues'],
             'grid_rows'                 => [Helper::class, 'sanitizeArrayKeysAndValues'],
+            'date_config'               => 'fluentform_sanitize_json_object',
             'enable_crop'               => 'sanitize_text_field',
             'crop_mode'                 => 'sanitize_text_field',
             'crop_ratio'                => 'sanitize_text_field',
@@ -176,7 +179,6 @@
             'crop_height'               => 'absint',
             'enforce_image_dimensions'  => 'sanitize_text_field',
         ];
-

         $settingsKeys = array_keys($settingsMap);

@@ -186,10 +188,10 @@
             'alt_text' => 'sanitize_text_field',
         ];
         $stylePrefKeys = array_keys($stylePrefMap);
-
+
         foreach ($fields as $fieldIndex => &$field) {
             $element = Arr::get($field, 'element');
-
+
             if ('container' == $element) {
                 $columns = $field['columns'];
                 foreach ($columns as $columnIndex => $column) {
@@ -245,7 +247,6 @@
                     }
                 }
             }
-

             if (!empty($field['attributes'])) {
                 $attributes = array_filter(Arr::only($field['attributes'], $attributesKeys));
@@ -268,7 +269,7 @@
                 $fields[$fieldIndex]['fields'] = $this->sanitizeFieldMaps($field['fields']);
                 continue;
             }
-
+
             if (!empty($field['style_pref'])) {
                 $settings = array_filter(Arr::only($field['style_pref'], $stylePrefKeys));

@@ -276,7 +277,7 @@
                     $fields[$fieldIndex]['style_pref'][$key] = call_user_func($stylePrefMap[$key], $value);
                 }
             }
-
+
             $validationRules = Arr::get($field, 'settings.validation_rules');
             if (!empty($validationRules)) {
                 foreach ($validationRules as $key => $rule) {
@@ -288,7 +289,7 @@
                 }
             }
         }
-
+
         return $fields;
     }

@@ -305,7 +306,7 @@

         FormMeta::persist($form->id, '_primary_email_field', $emailInputName);
     }
-
+
     private function sanitizeCustomSubmit($fields)
     {
         $customSubmitSanitizationMap = [
@@ -314,14 +315,14 @@
                 'borderColor'     => [$this, 'sanitizeRgbColor'],
                 'color'           => [$this, 'sanitizeRgbColor'],
                 'borderRadius'    => 'sanitize_text_field',
-                'minWidth'        => [$this, 'sanitizeMinWidth']
+                'minWidth'        => [$this, 'sanitizeMinWidth'],
             ],
             'normal_styles' => [
                 'backgroundColor' => [$this, 'sanitizeRgbColor'],
                 'borderColor'     => [$this, 'sanitizeRgbColor'],
                 'color'           => [$this, 'sanitizeRgbColor'],
                 'borderRadius'    => 'sanitize_text_field',
-                'minWidth'        => [$this, 'sanitizeMinWidth']
+                'minWidth'        => [$this, 'sanitizeMinWidth'],
             ],
             'button_ui'     => [
                 'type'    => 'sanitize_text_field',
@@ -331,7 +332,7 @@
         ];
         foreach ($fields as $fieldIndex => $field) {
             $element = Arr::get($field, 'element');
-
+
             if ('custom_submit_button' == $element) {
                 $styleAttr = ['hover_styles', 'normal_styles', 'button_ui'];
                 foreach ($styleAttr as $attr) {
@@ -344,8 +345,7 @@
                         }
                     }
                 }
-            }
-            elseif ('container' == $element) {
+            } elseif ('container' == $element) {
                 $columns = $field['columns'];
                 foreach ($columns as $columnIndex => $column) {
                     $fields[$fieldIndex]['columns'][$columnIndex]['fields'] = $this->sanitizeCustomSubmit($column['fields']);
@@ -369,7 +369,7 @@
         foreach ($stepWrapper as $fieldIndex => $field) {
             $element = Arr::get($field, 'element');

-            if ($element === 'step_start' || $element === 'step_end') {
+            if ('step_start' === $element || 'step_end' === $element) {
                 if (!empty($field['settings']['step_titles']) && is_array($field['settings']['step_titles'])) {
                     foreach ($field['settings']['step_titles'] as $index => $title) {
                         $field['settings']['step_titles'][$index] = fluentform_sanitize_html($title);
@@ -393,7 +393,7 @@
                         }
                     }
                 }
-
+
                 if (!empty($field['attributes']['class'])) {
                     $field['attributes']['class'] = sanitize_text_field($field['attributes']['class']);
                 }
@@ -402,7 +402,7 @@
                 }
             }

-            if ($element === 'step_start' && isset($field['fields'])) {
+            if ('step_start' === $element && isset($field['fields'])) {
                 $field['fields'] = $this->sanitizeStepsWrapper($field['fields']);
             }

@@ -419,7 +419,7 @@
         }
         return '';
     }
-
+
     public function sanitizeRgbColor($value) {
         if (preg_match('/^rgba?((d{1,3}s*,s*){2,3}(0|1|0?.d+))$/', $value)) {
             return $value;
--- a/fluentform/app/Services/FormBuilder/Components/DateTime.php
+++ b/fluentform/app/Services/FormBuilder/Components/DateTime.php
@@ -2,7 +2,7 @@

 namespace FluentFormAppServicesFormBuilderComponents;

-defined('ABSPATH') or die;
+defined('ABSPATH') || die;

 use FluentFormAppHelpersHelper;
 use FluentFormFrameworkHelpersArrayHelper;
@@ -24,7 +24,7 @@
             'fluentform_rendering_field_data_' . $elementName,
             [
                 $data,
-                $form
+                $form,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/rendering_field_data_' . $elementName,
@@ -54,11 +54,11 @@
         }
         $id = $data['attributes']['id'];

-        $ariaLabel = esc_html__(' Use arrow keys to navigate dates. Press enter to select a date.', 'fluentform') ;
-        $label = ArrayHelper::get($data,'settings.label');
-        $elMarkup = "<input  aria-label='".$label.$ariaLabel."'  aria-haspopup='dialog' data-type-datepicker data-format='" . esc_attr($dateFormat) . "' " . $atts . " aria-invalid='false' aria-required={$ariaRequired}>"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $atts is escaped before being passed in.
+        $ariaLabel = esc_html__(' Use arrow keys to navigate dates. Press enter to select a date.', 'fluentform');
+        $label = ArrayHelper::get($data, 'settings.label');
+        $elMarkup = "<input  aria-label='" . $label . $ariaLabel . "'  aria-haspopup='dialog' data-type-datepicker data-format='" . esc_attr($dateFormat) . "' " . $atts . " aria-invalid='false' aria-required={$ariaRequired}>"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $atts is escaped before being passed in.
         $config = $this->getDateFormatConfigJSON($data['settings'], $form);
-        $customConfig = $this->getCustomConfig($data['settings']);
+        $customConfig = $this->getCustomConfig($data['settings'], $form);
         $this->loadToFooter($config, $customConfig, $form, $id);
         $html = $this->buildElementMarkup($elMarkup, $data, $form);

@@ -67,7 +67,7 @@
             [
                 $html,
                 $data,
-                $form
+                $form,
             ],
             FLUENTFORM_FRAMEWORK_UPGRADE,
             'fluentform/rendering_field_html_' . $elementName,
@@ -126,7 +126,7 @@

         $config = apply_filters('fluentform/frontend_date_format', [
             'dateFormat'    => $dateFormat,
-            'ariaDateFormat'    =>"F j, Y",
+            'ariaDateFormat'    =>'F j, Y',
             'enableTime'    => $hasTime,
             'noCalendar'    => ! $this->hasDate($dateFormat),
             'disableMobile' => true,
@@ -136,15 +136,19 @@
         return json_encode($config, JSON_FORCE_OBJECT);
     }

-    public function getCustomConfig($settings)
+    public function getCustomConfig($settings, $form = null)
     {
-        $customConfigObject = trim(ArrayHelper::get($settings, 'date_config'));
+        $customConfigObject = fluentform_sanitize_json_object(
+            (string) ArrayHelper::get($settings, 'date_config')
+        );

-        if (! $customConfigObject || '{' != substr($customConfigObject, 0, 1) || '}' != substr($customConfigObject, -1)) {
-            $customConfigObject = '{}';
-        }
+        $customConfigObject = '' !== $customConfigObject ? $customConfigObject : '{}';

-        return $customConfigObject;
+        // The stored field value is always sanitised to a data-only JSON object
+        // (functions/expressions are stripped — they are the XSS vector for
+        // lower-privilege editors). Developers who need flatpickr callbacks
+        // supply them here from trusted server-side code, never via the setting.
+        return apply_filters('fluentform/date_time_custom_config', $customConfigObject, $settings, $form);
     }

     private function loadToFooter($config, $customConfigObject, $form, $id)
--- a/fluentform/app/Services/Manager/ManagerService.php
+++ b/fluentform/app/Services/Manager/ManagerService.php
@@ -14,7 +14,7 @@
     {
         $limit = Arr::get($attributes, 'per_page', 10);
         $page = Arr::get($attributes, 'page', 1);
-        $offset = $page == 1 ? 0 : ($page - 1) * $limit;
+        $offset = 1 == $page ? 0 : ($page - 1) * $limit;

         $query = new WP_User_Query([
             'meta_key'     => '_fluent_forms_has_role',
@@ -55,25 +55,37 @@
             'forms' => $forms,
         ]);
     }
-
+
     public function addManager($attributes = [])
     {
         $manager = Arr::get($attributes, 'manager');
-
+
         $this->validate($manager);
-
+
         $permissions = Arr::get($manager, 'permissions', []);
-
+
+        // Defense in depth: only a full WordPress administrator may grant the
+        // crown-jewel `fluentform_full_access` permission. A holder of that ACL
+        // permission is not necessarily a WP admin (managers can be delegated),
+        // so without this a compromised full-access session — e.g. a script
+        // running via XSS in a privileged session — could mint new full-access
+        // managers and escalate. Lesser permission grants are unaffected.
+        if (in_array('fluentform_full_access', (array) $permissions, true) && !current_user_can('manage_options')) {
+            $permissions = array_values(array_filter((array) $permissions, function ($permission) {
+                return 'fluentform_full_access' !== $permission;
+            }));
+        }
+
         $user = get_user_by('email', $manager['email']);
-
+
         if (!$user) {
             throw new ValidationException('', 0, null, ['message' => 'Please Provide Valid Email']);
         }
-
+
         Acl::attachPermissions($user, $permissions);
-
+
         update_user_meta($user->ID, '_fluent_forms_has_role', 1);
-
+
         $hasSpecificFormsPermission = 'yes' === Arr::get($manager, 'has_specific_forms_permission');
         $allowedForms = array_values(array_filter(array_map('intval', (array) Arr::get($manager, 'forms', []))));

@@ -92,83 +104,83 @@
             'first_name'  => $user->first_name,
             'last_name'   => $user->last_name,
             'email'       => $user->user_email,
-            'permissions' => Acl::getUserPermissions($user)
+            'permissions' => Acl::getUserPermissions($user),
         ];
-
+
         return ([
             'message' => __('Manager has been saved.', 'fluentform'),
-            'manager' => $updatedUser
+            'manager' => $updatedUser,
         ]);
     }
-
+
     public function removeManager($attributes = [])
     {
         $userID = intval(Arr::get($attributes, 'id'));
         $user = get_user_by('ID', $userID);
-
+
         if (!$user) {
             return ([
                 'message' => __('Associate user could not be found', 'fluentform'),
             ]);
         }
-
+
         Acl::attachPermissions($user, []);
-
+
         delete_user_meta($user->ID, '_fluent_forms_has_role');
-
+
         $deletedUser = [
             'id'          => $user->ID,
             'first_name'  => $user->first_name,
             'last_name'   => $user->last_name,
             'email'       => $user->user_email,
-            'permissions' => Acl::getUserPermissions($user)
+            'permissions' => Acl::getUserPermissions($user),
         ];
-
+
         return ([
             'message' => __('Manager has been removed.', 'fluentform'),
-            'manager' => $deletedUser
+            'manager' => $deletedUser,
         ]);
     }
-
+
     private function validate($manager)
     {
         $rules = [
             'permissions' => 'required',
             'email'       => 'required|email',
         ];
-
+
         $validatorInstance = new Validator();
         $validator = $validatorInstance->make($manager, $rules);
-
+
         $errors = null;
-
+
         if ($validator->validate()->fails()) {
             $errors = $validator->errors();
         }
-
+
         if (!isset($errors['email'])) {
             $user = get_user_by('email', $manager['email']);
-
+
             if (!$user) {
                 $errors['email'] = [
                     'no_user' => __('We could not found any user with this email.', 'fluentform'),
                 ];
             }
         }
-
+
         if (!isset($errors['permissions'])) {
             $message = $this->dependencyValidate($manager['permissions']);
-
+
             if ($message) {
                 $errors['permissions'] = [
                     'dependency' => $message,
                 ];
             }
         }
-
+
         if ($errors) {
             // Escape all error messages before throwing
-            $escapedErrors = array_map(function($errorMessages) {
+            $escapedErrors = array_map(function ($errorMessages) {
                 if (is_array($errorMessages)) {
                     return array_map('esc_html', $errorMessages);
                 }
@@ -181,22 +193,22 @@
             ]);
         }
     }
-
+
     private function dependencyValidate($permissions)
     {
         $allPermissions = Acl::getReadablePermissions();
-
+
         foreach ($permissions as $permission) {
             $depends = Arr::get($allPermissions, $permission . '.depends', []);
-
+
             if ($depends && $more = array_values(array_diff($depends, $permissions))) {
                 $message = $allPermissions[$permission]['title'] . ' requires permission: ';
-
+
                 foreach ($more as $i => $p) {
                     $joiner = $i ? ', ' : '';
                     $message = $message . $joiner . $allPermissions[$p]['title'];
                 }
-
+
                 return $message;
             }
         }
--- a/fluentform/boot/globals.php
+++ b/fluentform/boot/globals.php
@@ -440,6 +440,64 @@
     return preg_replace('/</?script[^>]*>/is', '', $content);
 }

+function fluentform_sanitize_json_object($value)
+{
+    if (!is_string($value) || '' === trim($value)) {
+        return '';
+    }
+
+    $value = trim($value);
+
+    $decoded = json_decode($value, true);
+
+    // Best-effort recovery of a pure-data JS-object literal (unquoted keys,
+    // single quotes, trailing commas) — the documented "JS object" format for
+    // the Date/Time field's advanced config. json_decode below is the security
+    // gate: any function or expression that survives normalisation is still not
+    // valid JSON, so it is rejected. Normalisation can only recover data, never
+    // execute or emit code.
+    if (JSON_ERROR_NONE !== json_last_error() || !is_array($decoded)) {
+        $decoded = json_decode(fluentform_js_object_to_json($value), true);
+    }
+
+    if (JSON_ERROR_NONE !== json_last_error() || !is_array($decoded)) {
+        return '';
+    }
+
+    if ([] === $decoded) {
+        return '{}';
+    }
+
+    // date_config must be an object; reject a top-level JSON array. Nested
+    // arrays (e.g. flatpickr `disable: [...]`) are preserved by not forcing
+    // JSON_FORCE_OBJECT recursively.
+    if (array_keys($decoded) === range(0, count($decoded) - 1)) {
+        return '';
+    }
+
+    return wp_json_encode($decoded);
+}
+
+function fluentform_js_object_to_json($value)
+{
+    // Single-quoted strings -> double-quoted (respecting escapes).
+    $value = preg_replace_callback(
+        "/'((?:\\.|[^'\\])*)'/s",
+        function ($m) {
+            return '"' . str_replace(['\'', '"'], [''', '\"'], $m[1]) . '"';
+        },
+        $value
+    );
+
+    // Quote unquoted object keys: `{ key:` / `, key:` -> `{ "key":`.
+    $value = preg_replace('/([{,]s*)([A-Za-z_$][A-Za-z0-9_$]*)(s*:)/', '$1"$2"$3', $value);
+
+    // Drop trailing commas before a closing brace/bracket.
+    $value = preg_replace('/,s*([}]])/', '$1', $value);
+
+    return $value;
+}
+
 /**
  * Sanitize inputs recursively.
  *
--- a/fluentform/fluentform.php
+++ b/fluentform/fluentform.php
@@ -4,7 +4,7 @@
 /**
  * Plugin Name: Fluent Forms
  * Description: Contact Form By Fluent Forms is the advanced Contact form plugin with drag and drop, multi column supported form builder plugin
- * Version: 6.2.5
+ * Version: 6.2.6
  * Author: Contact Form - WPManageNinja LLC
  * Author URI: https://fluentforms.com
  * Plugin URI: https://wpmanageninja.com/wp-fluent-form/
@@ -17,7 +17,7 @@
 defined('FLUENTFORM') or define('FLUENTFORM', true);
 define('FLUENTFORM_DIR_PATH', plugin_dir_path(__FILE__));
 define('FLUENTFORM_FRAMEWORK_UPGRADE', '4.3.22');
-defined('FLUENTFORM_VERSION') or define('FLUENTFORM_VERSION', '6.2.5');
+defined('FLUENTFORM_VERSION') or define('FLUENTFORM_VERSION', '6.2.6');
 defined('FLUENTFORM_MINIMUM_PRO_VERSION') or define('FLUENTFORM_MINIMUM_PRO_VERSION', '6.0.0');

 if (!defined('FLUENTFORM_HAS_NIA')) {

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.