Monday morning… cup of tea… check my emails and review the servers. Rats. There are ongoing PayPal payment gateway attacks on two client sites from a botnet. That’s my plans for-the-day sorted out.
It’s not the first time the official WooCommerce PayPal payment gateway plugin has popped up on my radar. The current version (3.1.0 at the time of writing) seems to have a loophole that bots are using to create orders.
NOTE: If you have any suggestions for a better/alternative PayPal plugin, let me know in the comments.
Background
On a WooCommerce site, it used to be that orders were created by one of two methods:
- The customer adds items to their cart, proceeds to the checkout, enters payment details then clicks “Place order”. If the payment is successful, the customer is redirected to a Thank You page.
- As website administrator would go in to the back-end of Woo, go to the Orders section, manually create a new order and set it to “Pending payment”.
Those two options worked fine for a long time.
With newer versions of WooCommerce, you can manipulate a shopping cart via the REST API. From a developer’s point-of-view, this is cleaner than messing about with Ajax callbacks and cookies. But it can also make it more difficult to use tools like reCaptchas, because there’s no front-end stage to render the reCaptcha.
The REST API is used by the block-mode cart and checkout pages. If your site uses the classic/shortcode cart & checkout pages, I suspect you can block the cart REST endpoints in “.htaccess” (needs testing).
Botnet symptoms
In the back-end of the WooCommerce sites, we were seeing hundreds of Failed orders where the botnet tried to create and approve orders with PayPal payments. Looking through the access logs, we see that the stages of creating a cart, setting customer details and creating/approving the order all came from different IP addresses. So the botnet seems to be co-ordinated by command & control servers.
Although PayPal rejected most of the order submissions, a few were approved. Ultimately, the client had to refund these to the card holder. The trick was in trying to catch these before the warehouse shipped anything.
Looking through the orders, it became apparent that all the UK orders shared some things in common:
- First & surname all in lowercase, e.g. “john smith”
- A company name was always specified, and it was always a single word
- The billing phone number was always specified, and it was always an invalid UK phone number
Resolution
My client’s sites both run the Spam Shield plugin, which has a WooCommerce integration. This is hookable, so we can create a small PHP function to catch these fake customers at the checkout stage (via the REST API and also via the traditional Ajax checkout).
Full disclosure: I wrote the Spam Shield plugin, and its back-end database is largely driven by data gathered from the Headwall Hosting firewall logs.
In short, I added the following snippet to each site’s child theme (functions.php)
/**
* 2025-09-08 PF. Catch fake customer data from September 2025's botnet
* PayPal abuse.
*
* https://headwall-hosting.com/blog/more-paypal-abuse-botnet-attacks/
*/
function custom_is_woo_order_fake( $is_probably_fake, $customer_data ) {
if ( $is_probably_fake ) {
// Pass through
} else {
$country_codes = array( 'GB' );
$has_company = ! empty( $customer_data['billing_company'] );
$full_name = trim(
$customer_data['billing_first_name'] . ' ' .
$customer_data['billing_last_name']
);
$is_lowercase_full_name = ! empty( $full_name ) &&
$full_name === strtolower( $full_name );
$is_uk_billing = ! empty( $customer_data['billing_country'] ) &&
in_array( $customer_data['billing_country'], $country_codes );
$is_bad_uk_phone = $is_uk_billing &&
! empty( $customer_data['billing_phone'] ) &&
preg_match( '/^[2-9]/', $customer_data['billing_phone'] ) === 1;
// Do these customer data look fake?
$is_probably_fake =
$is_lowercase_full_name &&
$has_company &&
$is_bad_uk_phone;
}
return $is_probably_fake;
}
add_filter(
'spam_shield_woo_is_order_probably_fake',
'custom_is_woo_order_fake', 10, 2
);By returning true when we detect fake customer data, Spam Shield writes a customer error log record, before aborting (thus preventing the order being created). The log record contains “SpamWooBlock IP=x.x.x.x”, where “x.x.x.x” is the IP address used by the botnet:
[...DATE...] '...SNIP... SpamWooBlock IP=x.x.x.x', referer: https://example.com/checkoutThe Spam Shield plugin automatically pushes the IP address to the Spam Shield database as suspected “spam”. But because we’re confident these are from a botnet, we can periodically extract the IP addresses from the website logs and add them to a firewall blocklist, with something like this:
# Extract botnet IP addresses from the website error logs: grep -hoE 'SpamWooBlock IP=[0-9\.:a-fA-F]+' /var/www/*/log/error.log \ | cut -d'=' -f2 | sort -u
With the PHP code snippet in place, the spam/fake orders stopped immediately. The botnets are still consuming server resources, because they’re still trying to create orders. But each time they try, the PHP thread simply dies (after recording the IP in the log).
An hourly cron job collates the SpamWooBlock IP addresses and puts them in to a 90 day IP blocklist, which is applied to all the firewalls on our network.
Wrapping up
If you don’t want to install Spam Shield and/or implement the above code snippets, you can install our IP blocklist. If you run cPanel on your server, you can pull the blocklist every hour by copying the URL from the download button.
In short… we’re protected until the botnet changes how they format their fake customer data. Unless we manage to block their entire collection of IP addresses (which is a nice thought).
