Skip to content

Bot protection

Arkazzae edited this page Oct 7, 2026 · 1 revision

Bot protection stops automated sign-ups, spam comments, password-reset floods, scripted logins and fake orders on the public forms of WordPress and WooCommerce. It has two layers. A honeypot guards registration, lost password and comments and needs no outside service. Cloudflare Turnstile guards login, registration, lost password, comments and checkout once its keys are in wp-config.php.

The module is off by default. Every Turnstile switch is on by default but does nothing until both Turnstile keys are set.

Module ID bots
Default Off
Available Always
WP-CLI None
Since 0.1.0

How it works

Protected forms

Form Honeypot Turnstile
Login: wp-login.php, login forms made with wp_login_form(), the WooCommerce My Account login No Yes
Registration: wp-login.php, the WooCommerce My Account registration Yes Yes
Lost password: wp-login.php, the WooCommerce lost password form Yes Yes
Comments and WooCommerce product reviews, from visitors who are not logged in Yes Yes
WooCommerce checkout: classic, block, and orders sent straight to the Store API No Yes

Every check is tied to the handler of one form: wp-login.php, WooCommerce's form handler, wp-comments-post.php and the Store API checkout route. Nothing else is checked:

  • wp-admin, including Send password reset for a user
  • WP-CLI and cron
  • REST API requests with application passwords
  • forms of other plugins, such as contact forms

There is no honeypot on login, because password managers fill in and send login forms at once, and none on checkout, because browser autofill fills hidden fields and the Store API cannot carry the field.

Comments are checked only for visitors who are not logged in. The checkout is checked for everyone, logged-in customers included.

The honeypot

On registration, lost password and comment forms, SafeHouse adds a block that is placed off-screen and hidden from screen readers. It holds a text field labelled "Leave this field empty" (shouse_url) and a hidden field shouse_proof. On the first key press, tap, click or input inside the form, SafeHouse's script copies a proof value into shouse_proof.

A submission passes only when the trap field is present and empty and the proof matches. Bots that fill every field, and bots that post the form without running its script, fail. The message is:

This looks like an automated submission. Please make sure JavaScript is on and try again.

The proof is the same for every visitor, so pages from a full-page cache keep working. It is derived from the site's AUTH_KEY and AUTH_SALT. If you change those keys, cached pages carry the old proof and every honeypot form on them fails until you clear the page cache.

Turnstile

Turnstile is Cloudflare's check that a visitor is a person. SafeHouse loads Cloudflare's script only on pages that show a protected form, or the checkout when Turnstile on checkout is on.

The widget runs in Cloudflare's interaction-only appearance: most visitors never see it. It becomes visible only when Cloudflare needs the visitor to act, inside the form (on the block checkout, above the order button), and then takes the width of the form.

If someone sends the form before Turnstile has finished, the browser holds the form for up to 15 seconds while it waits for the token, then sends it anyway and the server decides.

The server then asks Cloudflare whether the token is valid, with a timeout of 5 seconds. A token passes only when:

  • Cloudflare confirms it,
  • it was made for this form (each form has its own action name, such as shouse_login, so a token from one form cannot be used on another), and
  • Cloudflare reports that the widget ran on the host name of the site's WordPress address or site address.

The visitor's IP address is sent along only when SafeHouse knows the real visitor address (see Behind Cloudflare or a proxy).

A failed check shows:

The anti-bot check did not pass. Please wait for it to finish and try again.

A browser that cannot load Turnstile, for example because an extension blocks challenges.cloudflare.com, sends the placeholder token unreachable. Cloudflare rejects it, so that visitor gets the message above, whatever When Cloudflare cannot be reached says.

On the block checkout, the token travels in the HTTP header X-SHouse-Turnstile of the Store API request. SafeHouse checks every POST to the Store API checkout route (/wc/store/checkout and /wc/store/v1/checkout, including /checkout/<order ID> for paying an existing order), whatever the query string says. On the classic checkout, SafeHouse checks the token only when WooCommerce found no other error in the order, so the single-use token is not spent on an order that fails anyway.

What visitors see when a check fails

Form What happens
wp-login.php login, registration, lost password The form is shown again with the message.
WooCommerce login, registration, lost password WooCommerce shows the message as an error notice.
Classic checkout The checkout shows the message as an error. No order is placed.
Block checkout and the Store API HTTP 403 with the code shouse_bots and the message. No order is placed.
Comments and reviews WordPress's comment error page with the message and HTTP status 403. The comment is not saved.

A failed Turnstile check on a wp-login.php login also counts as a failed login for Login limits.

Settings

On SafeHouse → Modules → Bot protection, select Details.

Setting Default What it does
Honeypot on registration, lost password and comments On "An invisible trap field plus proof that a person used the form in a browser. Needs no outside service. Visitors with JavaScript turned off cannot register, reset a password or comment."
Turnstile on login On "wp-login.php, login forms made with wp_login_form() and the WooCommerce login form."
Turnstile on registration On "WordPress and WooCommerce My Account registration."
Turnstile on lost password On wp-login.php and the WooCommerce lost password form.
Turnstile on comments and reviews On "For visitors who are not logged in."
Turnstile on checkout On "Classic and block checkout, including orders sent straight to the Store API. Express payment buttons outside the checkout page (such as Apple Pay or Google Pay on product pages) cannot pass this check, so turn it off on sites that use them."
When Cloudflare cannot be reached Let the submission through and log it The other choice is Block the submission. See Choose what happens when Cloudflare cannot be reached.

The Turnstile switches have no effect until both keys are in wp-config.php. While the module runs, Details shows which state Turnstile is in:

Message Meaning
"Turnstile is off: add SHOUSE_TURNSTILE_SITE_KEY and SHOUSE_TURNSTILE_SECRET_KEY to wp-config.php. The honeypot works without them." One or both keys are missing.
"Turnstile is off: wp-config.php has Cloudflare test keys, which always give the same answer and are not allowed on a production site." See Use test keys on staging.
"Turnstile is on, with keys from wp-config.php." Turnstile runs on the forms whose switches are on.

Set up Turnstile

Get the keys from Cloudflare

Turnstile needs a Cloudflare account, which can be a free one. The site does not have to use Cloudflare for its DNS or traffic.

  1. In the Cloudflare dashboard, go to Turnstile.

  2. Select Add widget.

  3. In Widget name, enter a name you recognize, for example the site's domain.

  4. In Hostname management, add the host name of the site, for example example.com. If WordPress and the site use different host names in Settings → General, add both.

    SafeHouse refuses a token when Cloudflare reports a host name that is not the host of the site's WordPress address or site address.

  5. In Widget mode, choose Managed, the mode Cloudflare recommends.

  6. Select Create.

  7. Copy the site key and the secret key.

Add the keys to wp-config.php

  1. Open wp-config.php and add these two lines above the line that says to stop editing, with your own keys:

    define( 'SHOUSE_TURNSTILE_SITE_KEY', 'YOUR_SITE_KEY' );
    define( 'SHOUSE_TURNSTILE_SECRET_KEY', 'YOUR_SECRET_KEY' );
  2. Go to SafeHouse → Modules and switch on Bot protection.

  3. Select Details and check the Turnstile switches. Turn off Turnstile on checkout if the shop has express payment buttons outside the checkout page (see Shops with express payment buttons).

  4. Select Save Changes.

    Details shows "Turnstile is on, with keys from wp-config.php." On the Integrations tab, the Cloudflare row shows Turnstile as on.

To test, open wp-login.php in a private browser window and log in. Usually you see nothing of Turnstile: the interaction-only appearance keeps the widget hidden unless Cloudflare needs you to act.

Use test keys on staging

Cloudflare publishes test keys that always give the same answer, for example the always-pass pair:

define( 'SHOUSE_TURNSTILE_SITE_KEY', '1x00000000000000000000AA' );
define( 'SHOUSE_TURNSTILE_SECRET_KEY', '1x0000000000000000000000000000000AA' );

SafeHouse treats any key that starts with 1x, 2x or 3x followed by at least 15 zeros as a test key. It refuses test keys on a production site, so that a staging wp-config.php copied to production cannot switch the check off quietly. With test keys on production, Turnstile is off and Details says why.

A site counts as production when wp_get_environment_type() returns production. WordPress returns that when WP_ENVIRONMENT_TYPE is not set. To use test keys on a staging or development copy, declare its type in that copy's wp-config.php:

define( 'WP_ENVIRONMENT_TYPE', 'staging' );

The values local, development and staging all allow test keys. With test keys, SafeHouse skips the form and host name checks, because test tokens carry fixed values.

Choose what happens when Cloudflare cannot be reached

When Cloudflare cannot be reached decides what happens when the server cannot ask Cloudflare about a token:

  • the request to Cloudflare fails or takes longer than 5 seconds
  • Cloudflare answers with an HTTP status other than 200, or with something that is not JSON
  • Cloudflare answers that the secret key is missing or wrong, or reports an internal error

A wrong secret key is treated like an outage on purpose: a typo in wp-config.php then follows this setting instead of locking everyone out of wp-login.php.

Choice What happens
Let the submission through and log it (default) The form is accepted as if the check had passed. The activity log gets a bot_blocked warning with the reason cloudflare unavailable.
Block the submission The visitor sees "The anti-bot check is unavailable right now. Please try again in a few minutes." The log gets the same warning.

The help text on the settings page sums it up: "Applies only when this server cannot reach Cloudflare, or Cloudflare rejects the secret key. A visitor whose browser blocks Turnstile is still asked to try again."

Choose Block the submission only if you would rather lose real sign-ups, comments and orders during a Cloudflare outage than let bots through. Logins are blocked too, so with this choice and a broken secret key nobody can log in through wp-login.php while Turnstile on login is on. If that happens to you, see Recover from a lockout.

Shops with express payment buttons

Important

Express payment buttons outside the checkout page, such as Apple Pay or Google Pay on product or cart pages, send orders without a Turnstile token. With Turnstile on checkout on, every one of those orders is refused.

If the shop uses such buttons, turn off Turnstile on checkout. The same applies to headless shops and apps that send orders to the Store API checkout route without the X-SHouse-Turnstile header. The other forms stay protected.

With Wordfence and other plugins

  • When Wordfence Login Security has its reCAPTCHA on, Turnstile on login and Turnstile on registration are locked with "Handled by Wordfence Login Security, skipped here." With WooCommerce active, this happens only when Wordfence Login Security's WooCommerce integration is also on. The honeypot on registration still runs. Set SHOUSE_IGNORE_OVERLAPS to true to run both.
  • Login limits counts a failed Turnstile check on wp-login.php as a failed login.
  • Hardening's generic login error never replaces the Bot protection messages.
  • A theme or plugin form that posts to wp-login.php, wp-comments-post.php or the WooCommerce form handler but does not run the standard WordPress or WooCommerce form hooks lacks the honeypot fields, so every submission from it fails. Turn the honeypot off, or use the standard forms.

What it stores

  • Settings in the shouse_settings option. The keys stay in wp-config.php.
  • Activity log entries bot_blocked, for example "Bot protection on comments: honeypot. Repeats are not logged for 10 minutes." The reason is honeypot, turnstile or cloudflare unavailable (a warning; the others are info). One entry per form and reason every 10 minutes, so a bot flood cannot flood the log.
  • Transients shouse_bots_* that hold that 10-minute pause.

Bot protection sets no cookies, has no tables and sends no e-mails.

Limitations

  • The honeypot proof is one fixed value per site, and there is no timing check. A bot that loads the page and copies the value from it passes the honeypot. Turnstile is the stronger layer.
  • Visitors with JavaScript turned off cannot register, reset a password or comment while the honeypot is on, and fail every form where Turnstile is on.
  • Turnstile is usually invisible because it runs in interaction-only mode. You cannot tell from looking at a form whether it is protected; check Details and the activity log.
  • On the block checkout, the widget is placed only where the checkout block's order button area is in the page. A heavily changed checkout without it gets no widget, and orders then fail the check.
  • Express payment buttons outside the checkout page and headless Store API clients cannot pass Turnstile on checkout.
  • Forms of other plugins, such as contact forms, are not protected.
  • Rotating AUTH_KEY or AUTH_SALT breaks honeypot forms on cached pages until the page cache is cleared.

Related pages

Clone this wiki locally