CVE-2026-5069

Fluent Forms <= 6.2.1 - Incorrect Authorization to Authenticated (Subscriber+) Arbitrary Subscription Cancellation via 'subscription_id'

mediumIncorrect Authorization
5.4
CVSS Score
5.4
CVSS Score
medium
Severity
6.2.2
Patched in
1d
Time to patch

Description

The Fluent Forms plugin for WordPress is vulnerable to incorrect authorization via the 'subscription_id' parameter in versions up to, and including, 6.2.1. This is due to insufficient ownership authorization checks in the payment cancellation AJAX flow. This makes it possible for authenticated attackers, with subscriber-level access and above, to submit cancellation requests for other users' subscriptions.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=6.2.1
PublishedJuly 9, 2026
Last updatedJuly 10, 2026
Affected pluginfluentform

What Changed in the Fix

Changes introduced in v6.2.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the methodology for verifying **CVE-2026-5069**, an incorrect authorization vulnerability in Fluent Forms that allows authenticated users (Subscriber level and above) to cancel arbitrary subscriptions by manipulating the `subscription_id` parameter. ### 1. Vulnerability …

Show full research plan

This research plan outlines the methodology for verifying CVE-2026-5069, an incorrect authorization vulnerability in Fluent Forms that allows authenticated users (Subscriber level and above) to cancel arbitrary subscriptions by manipulating the subscription_id parameter.

1. Vulnerability Summary

The vulnerability exists in the payment management logic of Fluent Forms. When a subscription cancellation request is processed via AJAX, the plugin fails to verify if the currently authenticated user has the authority (ownership) over the specific subscription_id being cancelled. While the endpoint requires a valid nonce and authentication, it lacks a check to ensure the user_id associated with the subscription matches the wp_get_current_user()->ID.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: fluentform_cancel_subscription (or the REST API equivalent via fluentform/v1/subscriptions/cancel)
  • HTTP Method: POST
  • Payload Parameter: subscription_id (Integer)
  • Required Authentication: Subscriber level or higher.
  • Preconditions:
    • The plugin must have at least one form configured with a recurring payment/subscription (e.g., Stripe or PayPal).
    • The attacker must be logged in to an account with at least subscriber capabilities.

3. Code Flow

  1. Entry Point: The request hits admin-ajax.php with the action fluentform_cancel_subscription.
  2. Dispatch: The request is routed to a handler (typically in a payment-related controller, e.g., FluentForm\App\Services\Payments\PaymentAction::handleSubscriptionCancel).
  3. Missing Check: The handler retrieves the subscription_id from the $_POST or $_REQUEST array.
  4. Database Query: The code fetches the subscription record from the fluentform_subscriptions table.
  5. Processing: The code proceeds to call the payment gateway's cancellation API (Stripe/PayPal) and updates the internal database status to cancelled.
  6. Failure: Between steps 4 and 5, there is no verification that the user_id on the subscription record matches the requester's ID, nor a check for fluentform_manage_payments capabilities.

4. Nonce Acquisition Strategy

Fluent Forms typically localizes its nonces in the global fluent_forms_global_var object or specific payment objects.

  1. Identify the Trigger: Subscriptions are usually managed by users on a "My Account" or "Subscription Management" page created with a shortcode.
  2. Shortcode: Check for the presence of payment management shortcodes. If unknown, create a page with [fluentform_info] or a specific subscription list shortcode (often part of the Pro features, but the vulnerability is reported in the base plugin's handling of the IDs).
  3. Extraction:
    • Navigate to the page where a user would naturally cancel their own subscription.
    • Use the execution agent's browser_eval to extract the nonce:
      // Common locations for Fluent Forms nonces
      window.fluent_forms_global_var?.nonce || 
      window.fluentform_payment_config?.nonce || 
      document.querySelector('input[name="_fluentform_payment_nonce"]')?.value
      
  4. Action Name: The action string used for wp_create_nonce is likely fluentform_payment_nonce or fluentform_recurrent_cancel.

5. Exploitation Strategy

The exploit involves sending a crafted AJAX request as an authenticated Subscriber to cancel a subscription belonging to another user.

Request Details:

  • URL: http://[target-ip]/wp-admin/admin-ajax.php
  • Method: POST
  • Headers: Content-Type: application/x-www-form-urlencoded
  • Body:
    • action=fluentform_cancel_subscription
    • subscription_id=[VICTIM_SUBSCRIPTION_ID]
    • nonce=[EXTRACTED_NONCE]

6. Test Data Setup

  1. Users:
    • Victim: Subscriber user (ID: 5).
    • Attacker: Subscriber user (ID: 10).
  2. Subscription Data:
    • Create a form with a recurring payment.
    • Log in as Victim and submit the form to generate a subscription record in the fluentform_subscriptions table.
    • Record the id of this subscription (e.g., subscription_id = 1).
  3. Attacker Login: Log in as Attacker.

7. Expected Results

  • Successful Exploit: The server returns a 200 OK with a JSON body indicating success (e.g., {"success": true, "message": "Subscription cancelled successfully"}).
  • Impact: The subscription record in the database for the Victim now has a status of cancelled, and any linked recurring payment gateway would receive a cancellation request.
  • Unsuccessful Exploit (Patched): The server returns a 422 or 403 error with a message like "You do not have permission to perform this action."

8. Verification Steps

After performing the HTTP request, verify the state change via wp-cli:

  1. Check Subscription Status:
    wp db query "SELECT status FROM wp_fluentform_subscriptions WHERE id = [VICTIM_SUBSCRIPTION_ID]"
    
    Status should be 'cancelled' if successful.
  2. Check Submission Logs:
    wp db query "SELECT * FROM wp_fluentform_submission_meta WHERE meta_key = 'payment_log' ORDER BY id DESC LIMIT 1"
    
    Review the logs to see if the cancellation was triggered by the Attacker's user ID.

9. Alternative Approaches

If the admin-ajax.php action is protected by a different nonce or requires specific routing:

  1. REST API Route: Fluent Forms uses a REST wrapper. Try the request at:
    POST /wp-json/fluentform/v1/subscriptions/[VICTIM_SUBSCRIPTION_ID]/cancel
    • Header: X-WP-Nonce (obtained from wp_rest action).
  2. Status Toggle: Check if the vulnerability extends to the updateStatus route identified in api.php:
    POST /wp-json/fluentform/v1/submissions/{entry_id}/status
    • Parameter: status=cancelled.
    • Test if this allows bypassing payment-specific checks to terminate the billing cycle.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Fluent Forms plugin for WordPress is vulnerable to incorrect authorization via the 'subscription_id' parameter in versions up to 6.2.1. This allows authenticated attackers with Subscriber-level access or higher to cancel arbitrary subscriptions belonging to other users due to a lack of ownership validation and an over-permissive ACL system that grants broad access to users with any plugin-specific capability.

Vulnerable Code

// app/Modules/Acl/Acl.php line 161
public static function hasPermission($permissions, $formId = false)
{
    if ($formId && !FormManagerService::hasFormPermission($formId)) {
        return false;
    }
    $userCapability = static::getCurrentUserCapability();

    if ($userCapability) {
        return true;
    } else {
        if (current_user_can('fluentform_full_access')) {
            return true;
        }

        $permissions = (array) $permissions;

        foreach ($permissions as $permission) {
            $allowed = current_user_can($permission);

            if ($allowed) {
                // ... (logic to apply filters and return true)
            }
        }

        return false;
    }
}

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/fluentform/6.2.1/app/Modules/Acl/Acl.php	2026-04-16 11:38:36.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/fluentform/6.2.2/app/Modules/Acl/Acl.php	2026-04-23 12:58:12.000000000 +0000
@@ -159,38 +159,51 @@
         if ($formId && !FormManagerService::hasFormPermission($formId)) {
             return false;
         }
-        $userCapability = static::getCurrentUserCapability();
 
-        if ($userCapability) {
+        // Only explicit full-access users should bypass individual permission checks.
+        if (static::hasExplicitFullAccess()) {
             return true;
-        } else {
-            if (current_user_can('fluentform_full_access')) {
-                return true;
-            }
+        }
 
-            $permissions = (array) $permissions;
+        $grantedRole = static::getCurrentUserCapability();
 
-            foreach ($permissions as $permission) {
-                $allowed = current_user_can($permission);
+        foreach ((array) $permissions as $permission) {
+            $allowed = current_user_can($permission);
 
-                if ($allowed) {
-                    $allowed = apply_filters_deprecated(
-                        'fluentform_verify_user_permission_' . $permission,
-                        [
-                            $allowed,
-                            $formId
-                        ],
-                        FLUENTFORM_FRAMEWORK_UPGRADE,
-                        'fluentform/verify_user_permission_' . $permission,
-                        'Use fluentform/verify_user_permission_' . $permission . ' instead of fluentform_verify_user_permission_' . $permission
-                    );
+            // A granted role can satisfy scoped permissions, but never full access.
+            if (!$allowed && $grantedRole && 'fluentform_full_access' !== $permission) {
+                $allowed = true;
+            }
 
-                    return apply_filters('fluentform/verify_user_permission_' . $permission, $allowed, $formId);
-                }
+            if (!$allowed) {
+                continue;
             }
 
-            return false;
+            return static::filterPermissionCheck($permission, $allowed, $formId);
         }
+ 
+        return false;
     }

Exploit Outline

1. Log in as a user with Subscriber privileges or higher. 2. Obtain a valid security nonce from the global 'fluent_forms_global_var' JavaScript object or payment management metadata on the site's frontend. 3. Identify a 'subscription_id' belonging to another user (e.g., via numerical enumeration). 4. Send a POST request to 'wp-admin/admin-ajax.php' with the action 'fluentform_cancel_subscription' (or the REST API equivalent 'POST /wp-json/fluentform/v1/subscriptions/{id}/cancel') including the targeted 'subscription_id' and the nonce. 5. The plugin processes the cancellation request without verifying if the subscription record's 'user_id' matches the requester's ID, resulting in the cancellation of the victim's subscription and billing cycle.

Check if your site is affected.

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