SureForms <= 2.5.2 - Unauthenticated Payment Amount Validation Bypass via 'form_id'
Description
The SureForms – Contact Form, Payment Form & Other Custom Form Builder plugin for WordPress is vulnerable to Payment Amount Bypass in all versions up to, and including, 2.5.2. This is due to the create_payment_intent() function performing a payment validation solely based on the value of a user-controlled parameter. This makes it possible for unauthenticated attackers to bypass configured form payment-amount validation and create underpriced payment/subscription intents by setting form_id to 0.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:NTechnical Details
What Changed in the Fix
Changes introduced in v2.6.0
Source Code
WordPress.org SVN# Vulnerability Research Plan: CVE-2026-4987 - SureForms Payment Bypass ## 1. Vulnerability Summary The **SureForms** plugin (up to version 2.5.2) contains a logic flaw in its payment processing workflow. The function `create_payment_intent()` (likely located in `SRFM\Inc\Payments\Stripe\Stripe_Hel…
Show full research plan
Vulnerability Research Plan: CVE-2026-4987 - SureForms Payment Bypass
1. Vulnerability Summary
The SureForms plugin (up to version 2.5.2) contains a logic flaw in its payment processing workflow. The function create_payment_intent() (likely located in SRFM\Inc\Payments\Stripe\Stripe_Helper or a similar payment-handling class) is responsible for generating a Stripe/payment provider intent.
The vulnerability arises because the server-side validation of the payment amount is conditionally linked to a provided form_id. If an attacker provides a form_id of 0, the application fails to look up the "official" price for the form and instead trusts the user-provided amount parameter. This allows an unauthenticated user to create a payment intent for an arbitrary (lower) value than intended by the site administrator.
2. Attack Vector Analysis
- Endpoint: WordPress AJAX API (
/wp-admin/admin-ajax.php). - Action:
srfm_create_payment_intent(inferred from plugin prefixsrfmand function namecreate_payment_intent). - HTTP Method:
POST. - Authentication: Unauthenticated (requires
wp_ajax_nopriv_registration). - Vulnerable Parameter:
form_id(set to0) andamount. - Preconditions: The site must have Stripe or another payment provider configured within SureForms.
3. Code Flow
- Entry Point: A user interacts with a SureForms payment form on the frontend.
- AJAX Call: The frontend JS triggers a POST request to
admin-ajax.phpwith the actionsrfm_create_payment_intent. - Handler: The handler calls
create_payment_intent(). - Bypass Logic (Vulnerable Path):
- The code checks
$_POST['form_id']. - If
form_idis a valid post ID, it retrieves the form configuration:$form_settings = get_post_meta( $form_id, '_srfm_form_settings', true );. - It would then validate
$_POST['amount']against$form_settings['payment_amount']. - The Bug: If
form_idis0, the logic either skips the meta lookup or handles the empty result by defaulting to the value passed in theamountparameter without cross-referencing a source of truth.
- The code checks
- Sink: The unvalidated amount is passed to
Stripe_Helperto create aPaymentIntentvia the Stripe API.
4. Nonce Acquisition Strategy
SureForms likely localizes a nonce for frontend form submissions. Based on standard WordPress plugin patterns and the provided file list, the nonce is likely registered via wp_localize_script.
- Strategy:
- Identify a page containing a SureForms payment form.
- The plugin uses the prefix
srfm. The localized object is likely namedsrfm_varsorsrfm_frontend. - Action:
- Create a test payment form using the plugin.
- Place the form on a public page:
wp post create --post_type=page --post_status=publish --post_content='[sureforms id="XYZ"]'(where XYZ is the form ID). - Navigate to the page using
browser_navigate. - Execute
browser_eval("window.srfm_frontend?.nonce")orbrowser_eval("window.srfm_vars?.nonce")to extract the token.
- Verification: Check the
wp_verify_nonceorcheck_ajax_referercall in the source (if accessible) to confirm the action string (e.g.,srfm_nonceorsrfm_form_nonce).
5. Exploitation Strategy
Step-by-Step Plan:
- Target Identification: Find a published SureForms payment form and its legitimate
form_id. Note the actual price (e.g., $100.00). - Nonce Extraction: Access the page where the form is hosted and extract the AJAX nonce from the
srfm_frontendJS object. - Draft Malicious Request:
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
(Note:action=srfm_create_payment_intent &_ajax_nonce=[EXTRACTED_NONCE] &form_id=0 &amount=100 ¤cy=usd &payment_method_id=pm_card_visaamount=100represents $1.00 in Stripe's smallest currency unit).
- URL:
- Execution: Send the request via
http_request. - Observation: A successful exploit will return a
200 OKwith a JSON body containing a Stripeclient_secretor success message, despite theamountbeing incorrect for the site's forms.
6. Test Data Setup
- Configure Plugin: Activate
sureforms. - Mock Payment Setup: If a real Stripe key isn't available, check if the plugin has a "Test Mode" or "Offline Payment" mode.
- Create High-Value Form:
- Create a new form (Post Type:
srfm_forms). - Set the payment amount to
10000($100.00). - Get the
$REAL_FORM_ID.
- Create a new form (Post Type:
- Publish Page:
wp post create --post_type=page --post_title="Payment Page" --post_status=publish --post_content='[sureforms id="$REAL_FORM_ID"]'
7. Expected Results
- Success: The server returns a JSON response indicating a payment intent was created for the value
100(e.g.,{"success":true,"data":{"client_secret":"pi_..."}}). - Control Test: Sending the same request with the
$REAL_FORM_IDandamount=100should return an error (e.g.,Invalid amount).
8. Verification Steps
- Check Database: Query the
wp_srfm_entriestable (referenced inadmin/analytics.php) to see if a record was created with the manipulated amount.wp db query "SELECT * FROM wp_srfm_entries ORDER BY id DESC LIMIT 1;"
- Check Post Meta: If the plugin saves transient payment data, check the metadata for the most recent entry.
9. Alternative Approaches
- REST API: If the AJAX endpoint fails, check for a registered REST route.
admin/analytics.phpmentionsregister_rest_route. Look for a route likesureforms/v1/payments. - Parameter Variation: If
form_id=0is blocked by a global check, tryform_id=null,form_id[]=or a non-existent ID (e.g.,999999) to see if the fallback to the user-suppliedamountstill occurs. - Subscription Bypass: The description mentions "subscription intents." If the form supports recurring payments, try adding
plan_idorbilling_cycleparameters while keepingform_id=0.
Summary
The SureForms plugin for WordPress is vulnerable to a payment amount validation bypass in the `create_payment_intent()` function. Due to improper input validation, unauthenticated attackers can set the `form_id` parameter to 0, causing the server to skip price verification against form settings and trust a user-provided amount instead.
Security Fix
@@ -108,8 +108,6 @@ add_action( 'admin_menu', [ $this, 'maybe_add_entries_badge' ], 99 ); } - add_action( 'admin_menu', [ $this, 'add_payments_badge' ], 99 ); - add_filter( 'wpforms_current_user_can', [ $this, 'disable_wpforms_capabilities' ], 10, 3 ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_pointer' ] ); @@ -662,34 +660,6 @@ } /** - * Summary of add_payments_badge - * - * @since 2.0.0 - * @return void - */ - public function add_payments_badge() { - if ( ! Helper::current_user_can() ) { - return; - } - - global $submenu; - if ( isset( $submenu['sureforms_menu'] ) ) { - foreach ( $submenu['sureforms_menu'] as $index => $sub_item ) { - if ( isset( $sub_item[2] ) && SRFM_PAYMENTS === $sub_item[2] ) { - ob_start(); - ?> - <span style="color: #4ADE80;font-size: 9px;font-weight: 600;"><?php echo esc_html__( 'New', 'sureforms' ); ?></span> - <?php - $new_badge_html = ob_get_clean(); - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Adding notifications for submenu item. - $submenu['sureforms_menu'][ $index ][0] .= $new_badge_html; - break; - } - } - } - } - - /** * Mark the user's visit to the entries page. * * @since 1.7.3
Exploit Outline
The exploit targets the AJAX endpoint used for creating Stripe payment intents. An unauthenticated attacker first obtains a valid AJAX nonce from the site's frontend (usually found in the `srfm_frontend` or `srfm_vars` JavaScript objects). The attacker then sends a POST request to `/wp-admin/admin-ajax.php` with the action `srfm_create_payment_intent`. By providing a `form_id` of `0` and a self-defined `amount` parameter, the attacker forces the plugin to ignore the server-side price configuration associated with real forms. If the plugin has an active payment provider like Stripe configured, it will return a valid payment intent (e.g., a `client_secret`) for the attacker's lower amount, allowing them to complete a transaction for an arbitrary price.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.