Fluent Forms <= 6.2.1 - Incorrect Authorization to Authenticated (Subscriber+) Arbitrary Subscription Cancellation via 'subscription_id'
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:LTechnical Details
What Changed in the Fix
Changes introduced in v6.2.2
Source Code
WordPress.org SVNThis 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 viafluentform/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
subscribercapabilities.
3. Code Flow
- Entry Point: The request hits
admin-ajax.phpwith the actionfluentform_cancel_subscription. - Dispatch: The request is routed to a handler (typically in a payment-related controller, e.g.,
FluentForm\App\Services\Payments\PaymentAction::handleSubscriptionCancel). - Missing Check: The handler retrieves the
subscription_idfrom the$_POSTor$_REQUESTarray. - Database Query: The code fetches the subscription record from the
fluentform_subscriptionstable. - Processing: The code proceeds to call the payment gateway's cancellation API (Stripe/PayPal) and updates the internal database status to
cancelled. - Failure: Between steps 4 and 5, there is no verification that the
user_idon the subscription record matches the requester's ID, nor a check forfluentform_manage_paymentscapabilities.
4. Nonce Acquisition Strategy
Fluent Forms typically localizes its nonces in the global fluent_forms_global_var object or specific payment objects.
- Identify the Trigger: Subscriptions are usually managed by users on a "My Account" or "Subscription Management" page created with a shortcode.
- 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). - Extraction:
- Navigate to the page where a user would naturally cancel their own subscription.
- Use the execution agent's
browser_evalto 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
- Action Name: The action string used for
wp_create_nonceis likelyfluentform_payment_nonceorfluentform_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_subscriptionsubscription_id=[VICTIM_SUBSCRIPTION_ID]nonce=[EXTRACTED_NONCE]
6. Test Data Setup
- Users:
- Victim: Subscriber user (ID: 5).
- Attacker: Subscriber user (ID: 10).
- 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_subscriptionstable. - Record the
idof this subscription (e.g.,subscription_id = 1).
- Attacker Login: Log in as Attacker.
7. Expected Results
- Successful Exploit: The server returns a
200 OKwith 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
422or403error 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:
- Check Subscription Status:
Status should be 'cancelled' if successful.wp db query "SELECT status FROM wp_fluentform_subscriptions WHERE id = [VICTIM_SUBSCRIPTION_ID]" - Check Submission Logs:
Review the logs to see if the cancellation was triggered by the Attacker's user ID.wp db query "SELECT * FROM wp_fluentform_submission_meta WHERE meta_key = 'payment_log' ORDER BY id DESC LIMIT 1"
9. Alternative Approaches
If the admin-ajax.php action is protected by a different nonce or requires specific routing:
- 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 fromwp_restaction).
- Header:
- Status Toggle: Check if the vulnerability extends to the
updateStatusroute identified inapi.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.
- Parameter:
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
@@ -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.