View my basket

Fake card-testing orders in WooCommerce/PayPal

I’ve had multiple WooCommerce clients this year (2025) plagued by failed orders in WooCommerce. After lots of log-file analysis, it turns out to be a card-testing attack exploiting the WooCommerce/PayPal REST API.

The botnet card-testing attack

Instead of simulating a user navigating the website’s front-end, the bots create multiple low-value transactions using the site’s REST API. Most transactions are declined by PayPal, but some are authorised. Presumably the criminals behind the attack use the authorised cards to make cash withdrawals, before the cards are blocked by the issuer.

Dealing with the attack as it happened

The attacks are usually intense, with new fake orders created in waves. There would be 10 minutes of multiple orders every 10 seconds. Then there would be some respite of 10 minutes. The attacks are relentless, continuing until either the PayPal gateway is deactivated, or all their IP addresses are blocked. But they have an awful lot of IP addresses at their disposal.

Anatomy of the attack

Analysis of the web server access logs show the attack works with the REST API like this:

First… find the cheapest in-stock product in the shop:

GET /wp-json/wc/store/products?stock_status=instock&order=asc&orderby=price&min_price=100&...

Get the cart object as JSON then add the cheapest product to the cart (qty=1):

GET /wp-json/wc/store/cart
POST /wp-json/wc/store/cart/add-item

Set the customer’s billing details with some semi-random data:

POST /wp-json/wc/store/cart/update-customer

Create the order via the PayPal gateway

GET /checkout/
POST /?wc-ajax=ppc-data-client-id
POST /?wc-ajax=ppc-create-order

Creating a defence

The bot uses the same API endpoints used by the block-mode cart & checkout pages. So blocking the endpoints would break some website front-end functionality – not a realistic option. Which is a shame, because that would be easy – we could even do that in “.htaccess”.

The user billing data seem to be formatted similarly, with a lowercase first & last name, a single capitalised word for the billing company, and a US-style phone number (regardless of the billing country). I initially looked at building a defence around this and it worked for customers who only sell to the UK and Europe. But it would’ve blocked legitimate sales for my Woo customers that sell to the US, so I needed something else.

It’s all about cookies

I put in a trap to analyse which cookies exist when the checkout endpoints are requested, and it’s quite a minimal collection. Because the bot hasn’t visited the front-end of the site, it’s only got the bare-bones Woo cookies such as “woocommerce_items_in_cart” and “woocommerce_cart_hash”. There aren’t any Google Analytics (_ga) or cookie-consent cookies.

On one of my client’s sites, there’s a cookie that exists on every page, “cookieyes-consent”. This cookie is set by the CookieYes system and is present regardless of whether the user has consented to any additional cookies being present. So a bot that targets the REST API directly will not have this cookie, but a human visitor’s browser will have it.

So a snippet to block spammy card-test orders on sites that running CookieYes can work like this:

// Block direct access
defined( 'ABSPATH' ) || die();

function custom_block_rest_api_bot_orders() {
	// The (POST) URL paths we want to check against.
	$endpoints = array(
		'/wp-json/wc/store/checkout', // ...
		'/?wc-ajax=checkout',
		'/?wc-ajax=ppc-create-order',
		'/?wc-ajax=ppc-approve-order',
	);

	// Set this to a cookie that's on your site (but not a
	// standard Woo cookie).
	//
	// Some Examples:
	// If you use the CookieYes plugin: cookieyes-consent
	// if you have the Stripe payment gateway: __stripe_sid
	// If you have Google Analytics: _ga
	$required_cookie = 'cookieyes-consent';

	if ( ! is_array( $_SERVER ) || ! is_array( $_COOKIE ) ) {
		// Probably running from WP CLI
	} elseif ( ! array_key_exists( 'REQUEST_METHOD', $_SERVER ) ) {
		// Unknown request type.
	} elseif ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
		// Not a POST: we're not trying to create an order.
	} elseif ( ! array_key_exists( 'REQUEST_URI', $_SERVER ) ) {
		// No request path. Weird.
	} elseif ( ! in_array( $_SERVER['REQUEST_URI'], $endpoints ) ) {
		// The requested endpoint is not in our list.
	} elseif ( in_array( $required_cookie, $_COOKIE ) ) {
		// OK: The cookie is present.
	} else {
		// Add a customer message to the site's error logs,
		// which you can use to add the attacker's IP address
		// to your firewall.
		error_log( 'FakeOrderDetected' );
		wp_die( 'Suspicious activity detected' );
	}
}
add_action( 'init', 'custom_block_rest_api_bot_orders', 10 );

To summarise, if we’re trying to access one of the listed endpoints and the “cookieyes-consent” is not present, write a message to the site’s error log and then wp_die(). This will stop any further code executing, so the order will not be created.

If you’re not running CookieYes, you can use your browser’s dev tools to find a list of cookies your site uses, under the “Storage” tab.

Show website cookies in browser dev tools
Show the cookies used on any web page

IMPORTANT: This code has the ability to block orders in your Woo site’s checkout, so make sure you test it thoroughly. Make some test purchases as a guest user in a private browsing winder. Check your site’s error logs and/or use a plugin like Query Monitor to make sure things are running smoothly.

It’ll be interesting to see for how long this code will remain useful. I’m sure the bots will change tactic, or the Woo/PayPal will add another workflow vulnerability. But for now, this should help reduce a few Woo admin headaches.

8 thoughts on “Fake card-testing orders in WooCommerce/PayPal”

  1. This is genius. Such a great idea. I’ve been banging my head against many brick walls with this issue for months (might even be years) with PayPal and/or WooCommerce not acknowledging they have a problem.

    So far so good, thank you.

    Reply
    • I’m glad it’s working for you.

      I’ve been running it on some fairly big commerce sites for a few weeks now, and it seems to be holding strong. We even disabled the code for 24 hours to test something and the card-testing orders immediately started appearing again. So the code looks solid.

      Reply
  2. I just tried this on one of my customers sites, but it stops the PayPal element from working at checkout.

    Just comes up with a “Something went wrong” message.

    Reply
    • That suggests the required cookie wasn’t found in the headers. Did you verify which cookies are always used on your site and update the code snippet accordingly? You can email me directly if you’d like me to take a quick look at your site/checkout.

      Reply
      • I’m having the same issue; I verified that the cookie I’m testing for (sbjs_session) is present in the $_COOKIE variable but I get the “Something went wrong. Please try again or choose another payment source.” message when I try to check out via PayPal with the function enabled.

        Reply
        • Hi Jeff

          The “Something went wrong” message is coming from our new code snippet. Another user has this too, and my guess is that it’s to do with how the PayPal plugin is configured. I’ve not verified this though.

          You can edit the code snippet and add a line so it emails you the array of cookies – that’s how I developed the code.

          Before the line that references wp_die( … ), add a new line like this (change the email address):


          wp_mail( 'your-email@example.com', 'Checkout cookies', wp_json_encode( $_COOKIE ) );

          That will send you an email with the entire set of cookies (at the checkout). You should be able to see the difference between cookies from a legitimate checkout experience (a user who has interacted with the website in their browser) and a botnet. Emails from botnet checkout interactions should have fewer cookies.

          If cookies from a legitimate user’s checkout seem to be missing, it’s likely that PayPal is making a callback to the website from their servers (rather than the call coming from the browser).

          Can you verify any of that with your tests?

          Paul

          Reply
  3. Thanks Paul
    You saved my life, I added the Google Analytics cookie and work smooth so far.
    Paypal is already working on a fixing to block the attacks through API but so far your solution is the best!!!!!

    Reply

Leave a comment