From de7c0949de8d60bf1b660875002d31c4a127acff Mon Sep 17 00:00:00 2001 From: Jon Pither Date: Fri, 22 May 2026 10:29:51 +0100 Subject: [PATCH] xt26: honeypot to block bot form spam into Orbit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bots have been POSTing gibberish registrations straight to the public /.netlify/functions/xt26-register endpoint (gibberish names/company, dot-stuffed gmail addresses), which forwarded them into Orbit as prospect rows. The Orbit token isn't browser-exposed — it lives in this function's env — so the function endpoint itself is the gate. Adds a hidden off-screen `website` honeypot input to the shared contact form (off-screen rather than display:none so bots that skip hidden fields still trip it; removed from tab order so humans never focus it). The Netlify function silently drops any submission where it's non-empty — 200 OK, no forward to Orbit — so the bot can't detect the trap. Orbit's POST /api/people carries the same check as a backstop (juxt/orbit). Co-Authored-By: Claude Opus 4.7 (1M context) --- netlify/functions/xt26-register.ts | 13 +++++++++++++ src/components/ContactUsForm.tsx | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/netlify/functions/xt26-register.ts b/netlify/functions/xt26-register.ts index 5a0355310..2ba059a10 100644 --- a/netlify/functions/xt26-register.ts +++ b/netlify/functions/xt26-register.ts @@ -43,6 +43,19 @@ export const handler = async (event: NetlifyEvent): Promise => return json(400, { error: 'invalid json' }) } + // Honeypot. The form renders a hidden, off-screen `website` input + // that humans never fill. A non-empty value means the submission is + // almost certainly a bot — discard it WITHOUT forwarding to Orbit, + // and return 200 so the bot can't tell its submission was dropped + // (a 4xx would let it detect the trap and tune around it). This is + // the primary gate: bots POST straight to this public function + // endpoint, so rejecting here stops the spam at the edge. Orbit's + // POST /api/people carries the same check as a backstop. + if ((body.website ?? '').trim() !== '') { + console.warn('xt26-register: honeypot tripped, dropping submission') + return json(200, { ok: true }) + } + const firstName = (body.firstName ?? '').trim() const lastName = (body.lastName ?? '').trim() const email = (body.email ?? '').trim().toLowerCase() diff --git a/src/components/ContactUsForm.tsx b/src/components/ContactUsForm.tsx index fb6aca2c2..a4af6fef6 100644 --- a/src/components/ContactUsForm.tsx +++ b/src/components/ContactUsForm.tsx @@ -126,6 +126,22 @@ export function ContactUsForm(props: ContactUsFormProps) { value='c2bf653a-2baa-466d-bbcc-390272663918' /> + {/* Honeypot. Off-screen (not display:none — sophisticated bots + skip hidden fields) and removed from the tab order, so a + human never sees or focuses it and it stays empty. A bot + that auto-fills every input trips it; the Netlify function + and Orbit both silently discard any submission where it's + non-empty. Plausible-but-unused field name so bots target + it. See juxt/orbit docs/notes/website-form-spam.md. */} + + {/* Row 1: Name fields */}