Home → Plugins → Spam protection for Bricks forms
A public health clinic's website and an ordinary contact form built in Bricks that needed protecting from bots. There were two conditions: no third-party services, because patient data must not leave the server, and no puzzles to solve, because the site has to meet WCAG 2.1 AA. The result is a small mu-plugin: a honeypot, a time trap and a silent success response. Below is what we learned from the Bricks source code along the way.
Nothing leaves the server. No script from another domain, no API keys, no cookies. The decision whether a submission comes from a person is made entirely inside WordPress.
Visitors don't solve anything. No clicking on pictures, no retyping characters. For someone using a screen reader the form is exactly the same as before.
It covers every Bricks form. Including the ones added later — in a popup, in an off-canvas panel, on a new page — with no field to remember to add.
The bot never learns it was caught. It gets the same “sent” message a person does. The message goes nowhere, and the bot has no reason to change tactics.
Since version 1.12.2 any form field can be marked as a honeypot, and the spam protection section offers Turnstile, reCAPTCHA and hCaptcha. The built-in honeypot is worth enabling, and in many cases it is enough. Here a second layer was needed — a time trap — and the protection had to cover every form automatically, without editing each one in the builder.
The design was ready before the first line of PHP. Two of its assumptions changed after reading how Bricks accepts a form submission — and it was good that this happened before deployment rather than after.
The obvious idea is to name the hidden fields like form fields, with the form-field- prefix, so they are sure to be submitted. Bricks has a safeguard, though: any field with that prefix that is not in the form's saved settings makes it reject the whole submission as suspicious. A runtime PHP filter can't get around this, because the server checks what is stored in the database. So our fields have their own names. They are still submitted, because the Bricks front end collects the whole form, and the safeguard simply doesn't apply to them.
A time trap checks how long passed between the page loading and the form being sent. A timestamp printed by PHP would be stored in the page cache along with the rest of the HTML, and soon the trap would let everyone through — bots included. So the browser does the counting: a script records the start and writes the number of seconds on submit. A bot that posts the request without running JavaScript has no value at all — and that is a signal too.
bricks/form/validate is a filter, not an action: it receives an array of errors and the form object, and returns the array of errors. It runs after the nonce check, the built-in honeypot and the field list check, and before required-field validation and before the actions — sending the e-mail or redirecting. That is exactly the moment to decide, before anything leaves the server.
The Bricks front end shows the success message only when the response has success: true and data.type set to success. When spam is detected we send exactly that response, with the message taken from the form's settings, and end the request — the form actions never run. The built-in honeypot answers with an error by default; that can be changed with the bricks/element/form/honeypot/result filter.
add_filter( 'bricks/form/validate', function ( $errors, $form ) {
$hp = sanitize_text_field( wp_unslash( $_POST['zz_hp'] ?? '' ) );
$t = sanitize_text_field( wp_unslash( $_POST['zz_t'] ?? '' ) );
$spam = '' !== trim( $hp ) // honeypot filled in
|| ! is_numeric( $t ) // no time = no JS
|| (int) $t < 3; // too fast for a person
if ( ! $spam ) {
return $errors; // a person: Bricks does the rest
}
$msg = $form->get_settings()['successMessage'] ?? 'Thank you.';
wp_send_json_success( [ 'type' => 'success', 'message' => $msg ] );
}, 10, 2 );
Spam protection that stops bots but now and then also a genuine message is worse than spam. So these three things were checked in a real browser, on a staging copy of the site.
A form in a popup. A script that adds the fields once, after the page loads, doesn't see forms that appear later. Such a form would be sent without the time field, and the server would treat a person as a bot. A document observer adds the fields to forms inserted after the page has started as well.
Hiding the honeypot. The field has to disappear for the eye, the keyboard and the screen reader, yet stay in the form. It is moved off-screen, taken out of the tab order, hidden from assistive technology and has autofill switched off — so the browser doesn't fill it in on a person's behalf.
Four scenarios on one form. A submission like a person's — the e-mail arrived. A filled-in honeypot, a submission after one second and a request without JavaScript — each time the “sent” message and no e-mail. The time threshold is three seconds and can be changed with a single filter, without touching the code.
Tell us how many forms there are and whether the site has restrictions on third-party services. We'll say whether the built-in honeypot is enough or a second layer is worth adding.
Describe your form →Or by e-mail: info@asymetria.com.pl