Forminator – Contact Form, Payment Form & Custom Form Builder <= 1.52.0 - Missing Authorization to Unauthenticated Stripe PaymentIntent Reuse / Underpayment Bypass via 'paymentid' Parameter
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:NTechnical Details
What Changed in the Fix
Changes introduced in v1.52.1
Source Code
WordPress.org SVN# 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 viaForminator_CForm_Front_Action::$module_slugand parent class logic). - Vulnerable Parameter:
paymentid(within thePOSTdata). - Authentication: Unauthenticated (the plugin uses
wp_ajax_nopriv_for form submissions). - Preconditions:
- A Forminator form must be published with a Stripe field enabled.
- The attacker must possess a valid, successful Stripe
PaymentIntentID (pi_...) from a previous transaction on the same site.
3. Code Flow
- Entry Point: A
POSTrequest is sent toadmin-ajax.phpwithaction=forminator_submit_form_custom-forms. - AJAX Handling:
Forminator_Front_Action::handle_form_submit()is triggered. - Data Preparation: The plugin calls
Forminator_CForm_Front_Action::get_prepared_data(). - Payment Processing: If a Stripe field is present, the plugin checks for the
paymentidparameter. - Verification (Sink): The plugin uses
Forminator_Gateway_Stripe::get_payment_intent( $payment_id )to fetch the status from Stripe. - The Flaw: In
library/fields/stripe.php(and related processing logic), if the Stripe API returns asucceededstatus for the providedpaymentid, the plugin treats the form as paid. It lacks a check to ensurepayment_intent->amountmatches the form's calculated price or that thepayment_intent->metadataassociates it with the current form submission.
4. Nonce Acquisition Strategy
Forminator requires a specific nonce for form submissions: forminator_submit_form{form_id}.
Strategy:
- Identify Form ID: Determine the target form's ID (e.g.,
123). - Create/Visit Page: Create a page containing the target form's shortcode:
[forminator_form id="123"]. - Extract via Browser:
- Navigate to the page using
browser_navigate. - Forminator localizes its configuration in a global JavaScript object.
- Execute
browser_evalto 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
ForminatorFrontConfigunder the keyforminator_submit_form{id}.
- Navigate to the page using
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
- Configure Stripe: Ensure Forminator is connected to Stripe (Test Mode).
- Create Low-Value Form (ID: 1):
- Add a Stripe field.
- Set amount to "Fixed":
1.00. - Note the ID.
- Create High-Value Form (ID: 2):
- Add a Stripe field.
- Set amount to "Fixed":
100.00. - Note the ID.
- Publish: Place both forms on public pages using shortcodes.
- Generate PI: Submit Form 1 legitimately (using Stripe test cards) to get a successful
paymentid.
7. Expected Results
- The HTTP response should contain
"success":trueand 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:
- Find Entry ID:
wp db query "SELECT entry_id FROM wp_forminator_entries WHERE form_id = 2 ORDER BY entry_id DESC LIMIT 1;" - Verify Meta Data:
wp db query "SELECT meta_key, meta_value FROM wp_forminator_custom_form_entries WHERE entry_id = [ENTRY_ID];" - Check for 'payment_id': Ensure the
meta_valuefor the payment-related key matches thepi_...ID provided in the exploit. - Confirm Status: Check if the entry status is marked as finished/paid in the
wp_forminator_entriestable.
9. Alternative Approaches
- Parameter Variation: If
paymentidis not accepted directly, check if the value is pulled fromstripe-intentorstripe-payment-intent-idinside thePOSTbody, 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_formsto identify available forms and their associated price fields viawp post get [ID].
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
@@ -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() ); @@ -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.