← All docs

WordPress and WooCommerce

The WordPress plugin assesses the requests bots go for on a WordPress site — sign-ins, comments, XML-RPC, the REST API, and WooCommerce's sign-in and checkout — and acts on the answer before WordPress does. It is the PHP guard with WordPress's forms already named: nothing to configure but your keys.

It lives in integrations/wordpress/. php integrations/wordpress/build.php copies the PHP SDK into it and writes antibot-wordpress.zip, which you upload under Plugins → Add New → Upload Plugin. It needs WordPress 6.4 or later and PHP 8.1 or later.

Set it up

  1. Upload and activate the plugin.

  2. Under Settings → Antibot, paste:

    • a secret key with the assess use and nothing else;
    • your public key, for the agent;
    • your session keys (GET /api/management/v1/sessions/keys), so a request carrying a valid receipt passes without a call.

    Nothing is protected until a secret key is saved. The keys are never shown again on the page.

  3. Behind Cloudflare, a load balancer or a proxy, choose the header it sets for the visitor's address — see the visitor's address.

What is protected

Surface Default Assessed with
Sign-in (wp-login.php) on the account it names: log; user_email to register; user_login to reset a password
XML-RPC (xmlrpc.php) on —
Comments (wp-comments-post.php) on the comment's text
The REST API (/wp-json/, however the route is asked for) off —
WooCommerce checkout: classic (?wc-ajax=checkout, a form posted without JavaScript, and admin-ajax.php with action=woocommerce_checkout), block (Store API, alone or batched), order payment on with WooCommerce the billing email
WooCommerce My Account sign-in on with WooCommerce username
Front-end pages (every GET and HEAD WordPress answers itself, whatever the client says it accepts — curl's */* included; not feeds) off —

What each verdict does:

  • block: refused with a 403 and a reference the visitor can quote.
  • challenge: a sign-in, registration or password reset goes on to WordPress and is held with "Additional verification required", whatever the password; a comment is held for moderation; a WooCommerce checkout is refused with a WooCommerce notice (in the Store API's own error shape for the block checkout). A front-end page under the challenge posture gets the check-your-browser page first, unless it carries a receipt.
  • allow: WordPress goes on as usual.

The plugin declares itself compatible with WooCommerce's order tables (HPOS) — it reads and writes no order — and with the block checkout, which it holds through the Store API, so WooCommerce lists it as compatible with both.

Outcomes are reported as WordPress states them: wp_login is a success, wp_login_failed a failure, so a run of failures from one address reads as credential stuffing. A moderator approving a comment or marking it spam is reported too, so copies of a spam message are caught wherever they are posted next. To report that decision, the address and text a comment was assessed with are kept with it (comment meta _antibot); WordPress's personal data eraser removes them, and so does deleting the plugin.

Surfaces are recognised by the script WordPress is running, the method and the posted fields — not by the URL — so //wp-login.php, /%77p-json/, ;jsessionid and an absolute-form request line are what they are. admin-ajax.php with action=woocommerce_checkout, which WooCommerce still registers for visitors who are not signed in, is the checkout too — card testing through it once went unassessed.

The visitor's address

By default the plugin reads the connection's address (REMOTE_ADDR). Behind a proxy or CDN, choose the header it sets, and only one it sets on every request: anyone can send any header.

  • CF-Connecting-IP is believed from any connection. Choose it only when your server accepts connections from Cloudflare's addresses alone (a firewall that allows only Cloudflare's IP ranges, or Authenticated Origin Pulls); otherwise a client that reaches the server directly writes the header itself.
  • X-Forwarded-For takes the last address, the one the nearest proxy appended. That is right with exactly one proxy in front. Behind two (a CDN and a load balancer), the last address is the outer proxy's and every visitor shares it; with none, it is whatever the client wrote.
  • X-Real-IP for a proxy (nginx) that sets it.

A request without the chosen header is assessed by its connection's address, and a request with no address at all is still assessed.

Fields sent another way

The plugin reads a form's field (the sign-in's log, a comment's text, the billing email) from the body and query as sent. It reports the field as unread, rather than reading either value, when it is sent twice, sent beside a name PHP or another framework reads as the same field ( log, LOG, [log], PHP's folding of dots and spaces), hidden behind a ;, compressed, sent in a charset other than UTF-8, or in bytes that are not UTF-8. log=decoy&+log=admin — which WordPress signs in as admin, PHP having dropped the space — is one.

What it never does

  • Refuse an administrator. A signed-in user who can manage_options is neither assessed nor refused, so wp-admin stays reachable whatever an assessment says. Signing in is assessed like anyone else's, and xmlrpc.php discards every cookie before WordPress loads, so no one is signed in there. An administrator calling the REST API with an application password has no cookie, and is assessed.
  • Fail closed. If the API cannot be reached, or answers with an error, the request goes on. The settings page shows the last failure.
  • Send a password. Only the one field a form is assessed on leaves the site, with the method, URL and headers (never Cookie or Authorization).

Locked out? define( 'ANTIBOT_DISABLE', true ); in wp-config.php turns enforcement off until you remove it.

Monitor only

Under What is protected, tick Monitor only to see what enforcing would do before it does. Every decision is made as usual, but nothing is refused, challenged or held: a sign-in, a comment or a checkout goes on as if the plugin were not there, and sign-in outcomes are still reported. Each assessment is sent to the API marked as monitored, so the API keeps it as what would have been done, not as done. Use it for a site's first weeks, then untick it to enforce.

Where it runs

Enforcement runs on plugins_loaded, before any other plugin's work there. It is the earliest point at which WordPress can tell who is signed in (the cookie check exists only once pluggable.php has loaded), and it comes before WordPress adds slashes to the request's fields, so the account, the comment and the receipt are read exactly as sent.

A page served by a page cache never reaches WordPress, so the front-end setting cannot see it; sign-ins, comments, XML-RPC, REST and checkout always reach WordPress. For pages, put the edge in front.

For developers

  • antibot_surface (filter): which protected surface a request is, or null — for a sign-in at a URL of your own, return array( 'surface' => 'login', 'kind' => 'account', 'field' => 'log', 'action' => 'login' ).
  • antibot_refused (action): fires before a refusal or a challenge page is sent, with the surface and the assessment.
  • ANTIBOT_DISABLE (constant): enforces nothing while true.

Not covered yet: multisite's wp-signup.php, trackbacks, and plugin forms that post to admin-ajax.php (the antibot_surface filter can add them).

How it is tested

php integrations/wordpress/tests/run.php installs a real WordPress on SQLite (downloads checked against their sha1), installs and activates the plugin through wp-admin, and drives every surface over HTTP against a stand-in API that records each call: sign-in blocked and challenged, the account sent and the password never, outcomes, XML-RPC, comments and their moderation, receipts, the agent tag, the administrator bypass, the REST API however its route is spelled, the challenge posture, the client-address setting, fail-open, uninstall, and WooCommerce's sign-in and checkouts. CI runs it on the latest WordPress with WooCommerce, on the last 6.x and on 6.4, with WordPress Coding Standards.