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
Upload and activate the plugin.
Under Settings → Antibot, paste:
- a secret key with the
assessuse 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.
- a secret key with the
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
challengeposture 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_optionsis neither assessed nor refused, so wp-admin stays reachable whatever an assessment says. Signing in is assessed like anyone else's, andxmlrpc.phpdiscards 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, returnarray( '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.