Guide · Checkout hooks

Malware hooked into WooCommerce runs inside every order, on the server.

WooCommerce lets any plugin run its own code at fixed points in the checkout: before the form is shown, while the submitted fields are validated, after the order is created, when the payment completes. Extensions use those points constantly. So does server-side skimming malware — a few lines of PHP, attached to the same actions, that read what the customer submitted and send a copy somewhere else, or print a script into the checkout page. Nothing in the browser looks wrong, because the theft happens on your server. This guide shows how to list every checkout callback with the file it lives in, how to tell an extension’s hook from a rogue one, and what to do once you find it.

Symptom
Fraud on customers’ cards or accounts, with nothing visible at checkout
Mechanism
PHP attached to WooCommerce checkout and payment actions
What it can read
Everything the checkout form posts to your server
What it cannot read
Card numbers typed into a gateway’s hosted iframe

How it works

A checkout hook is a sanctioned place to run code — for anyone who can write a file

WooCommerce, like WordPress, is built on actions: named moments at which it calls every function registered for them. woocommerce_before_checkout_form fires as the checkout page is drawn. woocommerce_checkout_process fires while the submitted checkout fields are validated, before the order exists. woocommerce_checkout_order_processed fires once the order has been created from the classic checkout, and woocommerce_payment_complete when a payment is marked complete. Stores on the block-based checkout create orders through the Store API instead, which fires its own order-processed action; the search below includes it.

A callback on these actions runs as the site, on the server, with access to everything the request carries: billing name, address, email and phone, the order, the customer account, and any field the checkout posts to the server. Malicious code uses that access in one of two ways. It copies the submitted data — to a remote server, an email address, or a file in uploads that someone collects later — or it prints a script or a fake form into the checkout page, which turns a server-side foothold into browser-side skimming.

Because the code runs on the server and the checkout behaves normally, a visitor sees nothing and a browser-based check finds nothing. The rogue callback is usually small, often hidden in a must-use plugin, a theme’s functions.php, or an extra file inside a real plugin’s folder, and it frequently encodes what it sends so that log lines and outbound requests do not read as order data.

Find it

List every checkout callback, then explain each one

Both checks below only read. Run them from the WordPress root on a copy of the site where you can, and keep the output.

Figure 1 · Every callback, and where it lives

Three of these callbacks belong to software you can name. The fourth does not.

user@store:~/public_html
grep -rnE "add_action\(\s*['\"](woocommerce_checkout_process|...)" wp-content
wp-content/plugins/woocommerce/includes/class-wc-checkout.php:…
wp-content/plugins/example-gateway/includes/class-gateway.php:88: add_action( 'woocommerce_checkout_process', …
wp-content/themes/storefront-child/functions.php:41: add_action( 'woocommerce_before_checkout_form', …
wp-content/mu-plugins/wp-cache-helper.php:3: add_action('woocommerce_checkout_order_processed', function($id){
wp-content/mu-plugins/wp-cache-helper.php:5:   $d = base64_encode(json_encode($_POST)); wp_remote_post(…
# a cache helper has no reason to read the order or post it anywhere
A read-only search for add_action calls on the checkout hooks. Plugin, file and function names are illustrative; the one to explain is the callback in a file nothing installed. Illustrative reconstruction.
  1. 01

    Search the code for registrations on the checkout actions

    Every add_action or add_filter naming one of the checkout actions, across wp-content, with WooCommerce’s own folder left out of the result:

    shell
    grep -rnE "add_(action|filter)\(\s*['\"](woocommerce_(before_checkout_form|after_checkout_form|checkout_process|checkout_order_processed|payment_complete)|woocommerce_store_api_checkout_order_processed)['\"]" wp-content --include='*.php' | grep -v '^wp-content/plugins/woocommerce/'

    Each result is a file that runs code during checkout. Gateways, shipping, tax, analytics and checkout-field extensions all appear here legitimately; the list is what you now have to account for.

  2. 02

    Ask WordPress what is actually registered

    Code can register a callback without the literal hook name appearing in a file — built from a variable, or decoded at run time. With WP-CLI, print every callback on the main checkout actions with the file and line that defines it:

    WP-CLI
    wp eval 'foreach (array("woocommerce_before_checkout_form", "woocommerce_checkout_process", "woocommerce_checkout_order_processed", "woocommerce_payment_complete") as $h) { if (empty($GLOBALS["wp_filter"][$h])) continue; foreach ($GLOBALS["wp_filter"][$h]->callbacks as $p => $cbs) { foreach ($cbs as $cb) { $f = $cb["function"]; $r = is_array($f) ? new ReflectionMethod($f[0], $f[1]) : (is_string($f) && strpos($f, "::") ? new ReflectionMethod($f) : new ReflectionFunction($f)); echo "$h $p ", $r->getFileName(), ":", $r->getStartLine(), "\n"; } } }'

    The command loads the site’s plugins and theme the way a page view does, so run it on a copy or after preserving the evidence. A callback whose file is not in the first search’s results is the one to read first.

  3. 03

    Look for code that reads the checkout fields directly

    Rogue callbacks often take the raw posted fields rather than the order object:

    shell
    grep -rnE '\$_(POST|REQUEST)\[.?(billing_|card|cc_|stripe)' wp-content --include='*.php'

    Legitimate extensions do this too. What matters is where the data goes next: a remote request, mail, base64_encode, or a write to a file.

  4. 04

    Check the places that load without being listed

    Must-use plugins run automatically and do not appear on the Plugins screen; drop-ins load early; a theme’s functions.php runs on every request. Read each of these in full rather than relying on the search above.

    shell
    ls -la wp-content/mu-plugins
    ls -la wp-content/*.php

    Any file here that no one on the team can name is a finding in its own right.

Judge it

An extension’s hook, or a rogue one?

The hook name proves nothing on its own; hundreds of legitimate plugins use these actions. Judge the callback by where it lives and what it does.

It lives in software you can name and update

A callback inside a plugin you installed, at the version you installed, matching the vendor’s download, is explainable. One extra file inside that plugin’s folder is not — compare the folder against a fresh copy from the vendor.

It sends data somewhere the store has no reason to

wp_remote_post, curl, mail or a socket call to a domain that is not your gateway, your email provider or a service you contracted, inside a checkout callback, is the core of a skimmer.

It encodes or hides what it does

base64, gzip, string reversal, character codes, or a function name built from pieces, in code that handles order data. Legitimate checkout code has no reason to be unreadable.

It writes order data to a file

A callback that appends to a file in uploads, cache or a dot-file is storing data for someone to collect over the web or over SFTP.

It prints a script into the checkout

A callback on woocommerce_before_checkout_form or wp_footer that echoes a script tag pointing to an unfamiliar domain turns this into browser-side skimming; follow the fake payment form guide from there.

The file’s timestamps do not match its neighbours

Code added months after the plugin it sits in was last updated, or in the same window as other unexplained changes, belongs in the incident timeline.

Card data

What a hosted payment field protects — and what it does not

Gateways that draw the card number field inside their own iframe, or send the customer to a hosted payment page, keep the card number away from the store’s server entirely. A server-side callback cannot read what never reached the server, so on a store set up that way a PHP skimmer does not get the card number from the checkout post.

It still gets everything else: names, addresses, email addresses, phone numbers, order contents, and any custom checkout field. That is personal data with its own duties. And the same foothold that lets an attacker add a checkout callback lets them print a fake card form into the page, at which point card numbers are exposed after all. Stores whose gateway posts card details to the store’s own server — older or custom integrations — are exposed directly.

Respond

Preserve, contain, remove, rotate, verify

  1. 01

    Preserve the evidence

    Copy the rogue file with its timestamps and hash, the database, and the web server and PHP logs, before any edit. Note any file the callback writes to — it may hold the stolen data, and its size and timestamps date the exposure.

  2. 02

    Contain the checkout

    If the callback cannot be removed at once, take the checkout offline rather than leave it collecting. Tell your payment provider what you found; they will advise on the card side.

  3. 03

    Remove the callback and what planted it

    Delete the rogue file, replace any modified plugin or theme with a fresh copy from its vendor, then look for what wrote it: an unknown administrator, a loader in the database, a scheduled task, a vulnerable plugin, or stolen hosting credentials.

  4. 04

    Rotate everything the code could reach

    Gateway API keys and webhook secrets, WordPress administrators and application passwords, hosting, SFTP and database credentials, and any SMTP or API keys stored in WordPress.

  5. 05

    Verify and watch

    Re-run both checks above, then watch outbound connections and file changes for days, not hours. A callback that reappears means the cleanup missed a layer.

  6. 06

    Take advice on notification

    Exposure of customer personal data, and card data if any was reachable, can create duties to customers, card schemes and regulators. This guide is engineering guidance, not legal advice: establish what was exposed and since when, then take proper advice.

Common questions

Common questions about WooCommerce checkout hook malware

Is every add_action on a WooCommerce checkout hook suspicious?

No. Payment gateways, checkout-field editors, tax and shipping plugins, fraud screening and analytics all hook the checkout. The question for each callback is whether it belongs to software you installed and can update, and whether what it does with the data is explainable.

Our gateway uses a hosted iframe. Were card numbers exposed?

Not through a server-side callback alone, because the card number never reaches your server. Billing and personal data were reachable, and a foothold that can add a checkout callback can also print a fake card form into the page, so check the browser side before concluding.

Would a security plugin find this?

Possibly, if the code matches a known signature or the file differs from the plugin’s official copy. A short, custom callback in a must-use plugin or the theme can look like ordinary site code. Listing the registered callbacks and explaining each one is the reliable check.

Does this apply to the block-based checkout?

Yes, with different hooks. The Checkout block creates orders through the Store API, so some classic checkout actions do not fire there, and code targeting it can use the Store API’s own order-processed action or general hooks such as wp_footer. The search above already includes the Store API action.

We deleted the file and the fraud continued. What now?

Something else is still in place: another copy of the code, a loader that rewrites it, a script on the checkout page, or stolen credentials being reused. Preserve what you have, and treat it as a full incident rather than a single file.