1.7.1: security fixes for the checkout and the client IP
CI / WordPress latest + WooCommerce latest on PHP 8.3 (pull_request) Successful in 1m14s
Compatibility / WP latest / WC latest / PHP 7.4 (pull_request) Skipped
Compatibility / WP latest / WC beta / PHP 8.3 (pull_request) Skipped
Compatibility / WP latest / WC latest / PHP 8.3 (pull_request) Skipped
Compatibility / WP nightly / WC latest / PHP 8.3 (pull_request) Skipped
Compatibility / WP latest / WC latest / PHP 8.4 (pull_request) Skipped
Compatibility / Keep the compatibility headers honest (pull_request) Skipped
CI / Static checks (pull_request) Successful in 4m34s

- The discount follows the billing email, the address the order is
  placed under. The tracked session email used to win, so a guest could
  post a discounted address to the tracking endpoint and check out under
  their own, keeping the discount.
- Shipping packages carry the matched assignment, so WooCommerce's
  per-package rate cache no longer reuses rates filtered for a matched
  address (free shipping, reserved methods) after the email changes.
- The anonymous tracking endpoint no longer answers matched: true|false,
  which let anyone enumerate discounted addresses and domains.
- CF-Connecting-IP is believed only from a Cloudflare edge range or a
  private/loopback proxy. From anyone else it let a direct client pick a
  new address per request: no REST rate limit, forged audit-log IPs.

Smoke tests: 89 assertions (was 73), each fix with its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
kristianwind
2026-11-14 15:11:28 +01:00
co-authored by Claude Opus 5.5
parent 4a93a5fde8
commit d5d85612bd
8 changed files with 263 additions and 38 deletions
+59 -20
View File
@@ -38,6 +38,31 @@ class WC_EDM_Frontend {
add_action( 'woocommerce_cart_calculate_fees', array( $this, 'apply_discount_fees' ), 20 );
add_action( 'woocommerce_before_calculate_totals', array( $this, 'maybe_apply_coupon' ), 20 );
add_filter( 'woocommerce_package_rates', array( $this, 'filter_shipping_rates' ), 20, 2 );
add_filter( 'woocommerce_cart_shipping_packages', array( $this, 'key_packages_to_match' ) );
}
/**
* Put the customer's matched assignment into every shipping package.
*
* WooCommerce caches the rates it computed for a package in the session,
* keyed on a hash of the package — contents and destination, not who the
* customer is. Rates filtered for a matched address (free shipping, a
* reserved method) were therefore reused for the same cart after the
* email changed, including at the moment the order is placed. Putting the
* match in the package makes it part of that hash.
*
* @param array $packages Shipping packages.
* @return array
*/
public function key_packages_to_match( $packages ) {
$match = $this->get_current_match();
$key = $match ? (int) $match->id : 0;
foreach ( $packages as $index => $package ) {
$packages[ $index ]['wc_edm_match'] = $key;
}
return $packages;
}
/**
@@ -104,21 +129,28 @@ class WC_EDM_Frontend {
wp_send_json_error( array( 'message' => 'No session.' ) );
}
if ( $email && is_email( $email ) ) {
WC()->session->set( self::SESSION_EMAIL_KEY, $email );
} else {
WC()->session->set( self::SESSION_EMAIL_KEY, null );
$email = ( $email && is_email( $email ) ) ? $email : '';
WC()->session->set( self::SESSION_EMAIL_KEY, $email ? $email : null );
// The address is written where WooCommerce keeps the billing email,
// because that is what get_customer_email() reads first and what the
// checkout overwrites with the address the order is actually placed
// under. The discount therefore follows the order's own email, never
// one that was merely typed earlier.
if ( WC()->customer ) {
WC()->customer->set_billing_email( $email );
WC()->customer->save();
}
$this->clear_shipping_cache();
$match = $email ? WC_EDM_Helpers::find_email_match( $email ) : null;
wp_send_json_success(
array(
'matched' => (bool) $match,
)
);
// 🔴 Deliberately says nothing about whether the address matched.
// This endpoint answers anonymous visitors, and up to 1.7.0 it
// returned `matched: true|false` — one request per guess told a
// stranger which addresses and company domains the shop discounts.
// The checkout refresh this triggers is all the script needs.
wp_send_json_success();
}
/**
@@ -139,8 +171,15 @@ class WC_EDM_Frontend {
/**
* Resolve the email address of the current customer.
*
* Logged-in users use their account email; guests use the tracked
* session email or the billing email WooCommerce already knows.
* Logged-in users use their account email. Guests use the billing email
* WooCommerce holds for them — at checkout that is the address the order
* is placed under — and the tracked session email only when there is no
* billing email at all.
*
* 🔴 The order used to be the other way round: the tracked session email
* won. Anyone could post a discounted address to the tracking endpoint
* and then place the order under their own, taking the discount with
* nothing on the order pointing at the address that earned it.
*
* @return string
*/
@@ -152,13 +191,6 @@ class WC_EDM_Frontend {
}
}
if ( WC()->session ) {
$session_email = WC()->session->get( self::SESSION_EMAIL_KEY );
if ( $session_email ) {
return $session_email;
}
}
if ( WC()->customer ) {
$billing_email = WC()->customer->get_billing_email();
if ( $billing_email ) {
@@ -166,6 +198,13 @@ class WC_EDM_Frontend {
}
}
if ( WC()->session ) {
$session_email = WC()->session->get( self::SESSION_EMAIL_KEY );
if ( $session_email ) {
return (string) $session_email;
}
}
return '';
}