Commit 5088eccbdb for woocommerce
commit 5088eccbdbd6673b413d60679ab7c8f40b19d79a
Author: Mayisha <33387139+Mayisha@users.noreply.github.com>
Date: Thu Feb 5 22:51:40 2026 +0600
PayPal Standard : Add additional checks in shipping callback (#62990)
* add shipping callback related constants
* add a token param in shipping callback url
* validate the token in request
* add more validation checks
* fix lint
* fix phpstan
* use add_query_arg
* ignore incorrect type hint issue
* use hash_equals
* fix reverse logic
* update comment
* add cache class
* update constant name
* set and validate cached token
* fix phpstan
* add changelog
* increase expiration time
* fix lint
* store token in order meta
* validate token from order meta
* remove cache implementation
* clean up the key when order is approved
* check boolean instead of empty
* add @since
* remove unused constant
* add details in comment
* update version number in docblock
diff --git a/plugins/woocommerce/changelog/62990-add-additional-checks-in-paypal-standard-shipping-callback b/plugins/woocommerce/changelog/62990-add-additional-checks-in-paypal-standard-shipping-callback
new file mode 100644
index 0000000000..f94b4d035a
--- /dev/null
+++ b/plugins/woocommerce/changelog/62990-add-additional-checks-in-paypal-standard-shipping-callback
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Add additional validation in the PayPal Standard shipping callback method.
diff --git a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-paypal-standard-controller.php b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-paypal-standard-controller.php
index 1aca48e526..40738f27f3 100644
--- a/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-paypal-standard-controller.php
+++ b/plugins/woocommerce/includes/rest-api/Controllers/Version3/class-wc-rest-paypal-standard-controller.php
@@ -15,6 +15,8 @@ defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Gateways\PayPal\Helper as PayPalHelper;
use Automattic\WooCommerce\Gateways\PayPal\Request as PayPalRequest;
+use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants;
+use Automattic\WooCommerce\Enums\OrderStatus;
if ( ! class_exists( 'WC_Gateway_Paypal' ) ) {
require_once WC_ABSPATH . 'includes/gateways/paypal/class-wc-gateway-paypal.php';
@@ -60,11 +62,50 @@ class WC_REST_Paypal_Standard_Controller extends WC_REST_Controller {
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'process_shipping_callback' ),
- 'permission_callback' => '__return_true',
+ 'permission_callback' => array( $this, 'validate_shipping_callback_request' ),
)
);
}
+ /**
+ * Validate the shipping callback request.
+ *
+ * @since 10.6.0
+ * @param WP_REST_Request<array<string, mixed>> $request The request object.
+ * @return bool True if the request is valid, false otherwise.
+ */
+ public function validate_shipping_callback_request( WP_REST_Request $request ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
+ $token = $request->get_param( 'token' );
+ if ( empty( $token ) ) {
+ return false;
+ }
+
+ $purchase_units = $request->get_param( 'purchase_units' );
+ if ( empty( $purchase_units ) || empty( $purchase_units[0]['custom_id'] ) ) {
+ return false;
+ }
+
+ $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $purchase_units[0]['custom_id'] );
+ if ( ! $order ) {
+ return false;
+ }
+
+ // If shipping callback token is not stored in order meta, return true for this order as the token is not generated for the original order.
+ // We will not validate the token if the order did not generate a token in the create order request.
+ // This is done to prevent orders created before the shipping callback token feature was introduced from being blocked from updating their shipping details.
+ if ( ! $order->meta_exists( PayPalConstants::PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN ) ) {
+ return true;
+ }
+
+ $shipping_callback_token = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN, true );
+
+ if ( empty( $shipping_callback_token ) || ! hash_equals( $token, $shipping_callback_token ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
/**
* Callback for when the customer updates their shipping details in PayPal.
* https://developer.paypal.com/docs/checkout/standard/customize/shipping-module/#server-side-shipping-callbacks
@@ -95,7 +136,7 @@ class WC_REST_Paypal_Standard_Controller extends WC_REST_Controller {
// Compare PayPal order IDs.
$paypal_order_id_from_order_meta = $order->get_meta( '_paypal_order_id', true );
- if ( $paypal_order_id !== $paypal_order_id_from_order_meta ) {
+ if ( empty( $paypal_order_id_from_order_meta ) || $paypal_order_id !== $paypal_order_id_from_order_meta ) {
WC_Gateway_Paypal::log(
'PayPal order ID mismatch. Order ID: ' . $order->get_id() .
'. PayPal order ID (request): ' . $paypal_order_id .
@@ -105,6 +146,28 @@ class WC_REST_Paypal_Standard_Controller extends WC_REST_Controller {
return new WP_REST_Response( $response, 422 );
}
+ // Validate that the order is in a valid state for shipping updates.
+ // Only draft or pending orders should accept shipping updates.
+ if ( ! in_array( $order->get_status(), array( OrderStatus::CHECKOUT_DRAFT, OrderStatus::PENDING ), true ) ) {
+ WC_Gateway_Paypal::log(
+ 'Order is not in a valid state for shipping updates. Order ID: ' . $order->get_id() .
+ '. Order status: ' . $order->get_status()
+ );
+ $response = $this->get_update_shipping_error_response();
+ return new WP_REST_Response( $response, 422 );
+ }
+
+ // If the order has a PayPal transaction ID, a charge has already occurred, so we shouldn't change the shipping address.
+ $transaction_id = $order->get_transaction_id();
+ if ( ! empty( $transaction_id ) ) {
+ WC_Gateway_Paypal::log(
+ 'Order already has a transaction ID, cannot update shipping. Order ID: ' . $order->get_id() .
+ '. Transaction ID: ' . $transaction_id
+ );
+ $response = $this->get_update_shipping_error_response();
+ return new WP_REST_Response( $response, 422 );
+ }
+
if ( ! WC()->session ) {
WC()->session = new WC_Session_Handler();
}
diff --git a/plugins/woocommerce/src/Gateways/PayPal/Constants.php b/plugins/woocommerce/src/Gateways/PayPal/Constants.php
index 2e86c9d1f5..ef27a2d442 100644
--- a/plugins/woocommerce/src/Gateways/PayPal/Constants.php
+++ b/plugins/woocommerce/src/Gateways/PayPal/Constants.php
@@ -206,4 +206,13 @@ class Constants {
* @since 10.5.0
*/
public const PAYPAL_ORDER_META_PAYMENT_SOURCE = '_paypal_payment_source';
+
+ /**
+ * Meta key for storing PayPal shipping callback token in order meta.
+ *
+ * @var string
+ *
+ * @since 10.5.0
+ */
+ public const PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN = '_paypal_shipping_callback_token';
}
diff --git a/plugins/woocommerce/src/Gateways/PayPal/Request.php b/plugins/woocommerce/src/Gateways/PayPal/Request.php
index afa0512d0f..f341aea74f 100644
--- a/plugins/woocommerce/src/Gateways/PayPal/Request.php
+++ b/plugins/woocommerce/src/Gateways/PayPal/Request.php
@@ -636,9 +636,16 @@ class Request {
),
true
) ) {
+ $shipping_callback_token = $this->generate_shipping_callback_token( $order );
+ $callback_url = add_query_arg(
+ 'token',
+ $shipping_callback_token,
+ rest_url( 'wc/v3/paypal-standard/update-shipping' )
+ );
+
$params['payment_source'][ $payment_source ]['experience_context']['order_update_callback_config'] = array(
'callback_events' => array( 'SHIPPING_ADDRESS', 'SHIPPING_OPTIONS' ),
- 'callback_url' => $this->normalize_url_for_paypal( rest_url( 'wc/v3/paypal-standard/update-shipping' ) ),
+ 'callback_url' => $this->normalize_url_for_paypal( $callback_url ),
);
}
@@ -907,6 +914,23 @@ class Request {
);
}
+ /**
+ * Generate and store a shipping callback token for the order.
+ * The token is stored in the database cache and can be validated later.
+ *
+ * @param WC_Order $order The order object.
+ * @return string The generated token.
+ */
+ private function generate_shipping_callback_token( WC_Order $order ): string {
+ $token = bin2hex( random_bytes( 32 ) );
+
+ // Store the token in order meta for validation.
+ $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN, $token );
+ $order->save();
+
+ return $token;
+ }
+
/**
* Normalize PayPal order shipping country code.
*
diff --git a/plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php b/plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php
index ad30eeeceb..f04e02ecf9 100644
--- a/plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php
+++ b/plugins/woocommerce/src/Gateways/PayPal/WebhookHandler.php
@@ -85,6 +85,11 @@ class WebhookHandler {
if ( PayPalConstants::STATUS_APPROVED === $status ) {
\WC_Gateway_Paypal::log( 'PayPal payment approved. Order ID: ' . $order->get_id() );
$order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_STATUS, $status );
+ // Clear the shipping callback token by setting it to an empty string.
+ // This is done to prevent the token from being used again for the same order.
+ // We are not deleting the meta key as we use the existence of the meta key to determine if the token was ever generated for this order.
+ $order->update_meta_data( PayPalConstants::PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN, '' );
+ $order->save();
$order->add_order_note(
sprintf(
/* translators: %1$s: PayPal order ID */