Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/woocommerce-ip-history.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 1 addition & 2 deletions includes/class-webdecoy-woocommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand Down
49 changes: 49 additions & 0 deletions tests/WooProxyIPTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

require_once __DIR__ . '/WooOrderStatusTest.php';
require_once dirname(__DIR__) . '/sdk/src/SignalCollector.php';

function webdecoy()
{
return $GLOBALS['wd_woo_proxy_plugin'];
}

class WooProxyTestPlugin
{
public array $proxies = ['10.0.0.0/8'];

public function get_client_ip(): string
{
return (new \WebDecoy\SignalCollector($this->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');
});
Loading