diff --git a/docs/woocommerce-ip-history.md b/docs/woocommerce-ip-history.md new file mode 100644 index 0000000..b62b6d3 --- /dev/null +++ b/docs/woocommerce-ip-history.md @@ -0,0 +1,14 @@ +# Checkout IP history before the proxy-resolution fix + +Before the fix for WebDecoy/app#878, WooCommerce checkout attempts used a +collector without the plugin's configured trusted proxies. On proxied stores, +those records can contain the proxy address instead of the shopper address. + +Treat IP-based checkout velocity and card-testing history from versions without +this fix as unreliable on those stores. The affected rows do not retain enough +forwarding evidence to reconstruct the original shopper IP safely, so no +automatic backfill is performed. Do not interpret a shared proxy address as one +shopper or use it to justify a sitewide block. + +Record the site's plugin upgrade time when deploying this fix; only subsequent +checkout attempts use the same configured resolver as the rest of the plugin. diff --git a/includes/class-webdecoy-woocommerce.php b/includes/class-webdecoy-woocommerce.php index 04413bc..b348568 100644 --- a/includes/class-webdecoy-woocommerce.php +++ b/includes/class-webdecoy-woocommerce.php @@ -500,8 +500,7 @@ private function forward_to_webdecoy(string $ip, string $source, int $score, arr */ private function get_client_ip(): string { - $collector = new \WebDecoy\SignalCollector(); - return $collector->getIP(); + return webdecoy()->get_client_ip(); } /** diff --git a/tests/WooProxyIPTest.php b/tests/WooProxyIPTest.php new file mode 100644 index 0000000..1484026 --- /dev/null +++ b/tests/WooProxyIPTest.php @@ -0,0 +1,49 @@ +proxies))->getIP(); + } +} + +TestRunner::test('WooCommerce shares the configured client IP resolver', function () { + $saved = $_SERVER; + $GLOBALS['wd_woo_proxy_plugin'] = new WooProxyTestPlugin(); + $woo = (new ReflectionClass(WebDecoy_WooCommerce::class))->newInstanceWithoutConstructor(); + try { + foreach (['203.0.113.11', '203.0.113.12'] as $shopper) { + $_SERVER = ['REMOTE_ADDR' => '10.0.0.5', 'HTTP_X_FORWARDED_FOR' => $shopper]; + TestRunner::assertSame($shopper, $woo->get_client_ip_public(), 'shoppers must not share the proxy bucket'); + } + $GLOBALS['wd_woo_proxy_plugin']->proxies = []; + TestRunner::assertSame('10.0.0.5', $woo->get_client_ip_public(), 'untrusted headers stay untrusted'); + $GLOBALS['wd_woo_proxy_plugin']->proxies = ['10.0.0.0/8']; + $_SERVER = ['REMOTE_ADDR' => '203.0.113.21', 'HTTP_X_FORWARDED_FOR' => '192.0.2.99']; + TestRunner::assertSame('203.0.113.21', $woo->get_client_ip_public(), 'direct requests cannot spoof their IP'); + } finally { + $_SERVER = $saved; + unset($GLOBALS['wd_woo_proxy_plugin']); + } +}); + +TestRunner::test('checkout cannot reintroduce an unconfigured IP collector', function () { + $source = file_get_contents(dirname(__DIR__) . '/includes/class-webdecoy-woocommerce.php'); + preg_match('/private function get_client_ip\(\): string\s*\{([^}]+)\}/', $source, $match); + TestRunner::assertTrue(isset($match[1]), 'client IP method must exist'); + TestRunner::assertTrue(strpos($match[1], 'SignalCollector') === false, 'delegate instead of constructing an IP collector'); + TestRunner::assertTrue(strpos($match[1], 'webdecoy()->get_client_ip()') !== false, 'use the configured resolver'); +});