Actions
Feature #3918
open
MR
MR
Implement CF7 rate limiting + reCAPTCHA v3 bot protection
Feature #3918:
Implement CF7 rate limiting + reCAPTCHA v3 bot protection
Start date:
10/08/2026
Due date:
10/09/2026 (21 days late)
% Done:
100%
Estimated time:
8:00 h
Source:
Description
Goal: Stop spam/bot submissions on all four KFG WordPress sites (Odyssey, Redwood, Willow, RedwoodCOE) without silently dropping any genuine lead — the brief's stated priority, since the client's existing pipeline had no protection at all. Built and verified on Odyssey first, then ported identically (function/constant prefixes swapped per site: odyssey_/redwood_/willow_/redwoodcoe_) to the other three.
Rate limiting — 5 submissions per 24 hours, global, three identifiers
- Requirement was "no more than 5 per IP, email, or phone number within 24 hours," implemented literally: three independent counters (visitor IP, lowercased email, digits-only phone), and exceeding the limit on any one of them blocks the submission.
- Global across all forms on a site, not per-form — a deliberate choice. With three separate contact forms live on a site, a per-form quota would silently allow 15 submissions a day instead of 5, and anyone abusing the forms would just rotate between them.
- Counters are WordPress transients (wp_options), keyed on an MD5 hash of the identifier rather than the raw value, since storing plaintext IPs/emails/phone numbers in that table is more personal data than necessary.
- Counts only successful sends (hooked to wpcf7_mail_sent), so a visitor who fails validation several times and then succeeds has only used one of their five attempts.
- The 24-hour window starts on the first counted submission and is never extended by later ones in the same window — otherwise a slow, steady trickle of submissions would keep resetting the clock and the limit would never actually trigger.
- A blocked visitor is told plainly (a replaced form-footer message), rather than silently dropped — treated as the same category of problem as a lost lead, since a visitor who thinks their message was sent won't try again.
- An IP allowlist filter exists for office/testing addresses to exempt them from the count.
- Verified on all four sites that REMOTE_ADDR can safely be trusted (no CDN/proxy in front — direct Apache, each site's own nameservers, no cf-ray/via/x-cache headers); flagged clearly in code that this assumption breaks the whole mechanism if a CDN is ever added later, since the limit would then apply to one shared address for every visitor.
reCAPTCHA v3 — classify every submission, block none by default
- Rather than calling Google's siteverify endpoint a second time (which the brief's spec implied), the classifier hooks CF7's own wpcf7_recaptcha_verify_response filter and reads the score CF7's existing reCAPTCHA module already fetched — a second direct call to the same token would be rejected by Google as timeout-or-duplicate and break every submission.
- Four classification states: genuine (score ≥ 0.5), suspicious (≥ 0.3), spam (below 0.3), and unverified (verification itself failed — treated distinctly from a genuine low score, since CF7 internally reports an absent score as 0, which would otherwise be indistinguishable from real spam).
- The classification is written into hidden form fields (lead_status, recaptcha_score, recaptcha_errors) so every submission — regardless of score — still reaches Salesforce, carrying its own risk assessment for the sales team to filter on manually.
- Blocking is off by default on all four sites (*_RECAPTCHA_BLOCK = false). This directly fixes a pre-existing bug: the CRM Perks Salesforce plugin hooks wpcf7_before_send_mail, which CF7 never fires once a submission is marked spam — so any false positive under the old setup silently discarded a real lead with no record anywhere and told the visitor their message looked like spam.
- Fixed a separate, unrelated bug found during this work: the theme's existing reCAPTCHA-delay script was caching and reusing tokens for up to 90 seconds to reduce API calls, but Google penalizes reused tokens heavily — this was the actual cause of a prior complaint that "genuine visitors are scoring 0.1." Removing the reuse took an Odyssey test submission from 0.1 to 1.0 on the next attempt.
- Flamingo's built-in Spam folder is kept functional as a manual review queue (not a hard block) via wpcf7_flamingo_inbound_message_parameters, flagging only the lowest-scoring submissions; items that land there were still pushed to Salesforce, so the folder means "worth a second look," not "blocked," and the filter never clears a spam flag another plugin (e.g. Akismet) already set.
No data to display
Actions