From 9fc9e68c6cc7badd0342f15ad3205cd333cd8f4d Mon Sep 17 00:00:00 2001 From: Charles Dyas <80225316+dyascj@users.noreply.github.com> Date: Fri, 31 Jul 2026 15:50:20 -0400 Subject: [PATCH] fix(contact): reject submissions with no Turnstile token again The permissive setting was a stopgap from when the widget was not rendering. It let anything without a token through, which waves past the crudest spam: a bot that never runs JavaScript produces no token at all, so Turnstile was only catching the ones that tried and failed. Now that the widget is confirmed working for real visitors, a missing token is refused again. The message points anyone whose blocker eats challenges.cloudflare.com at the other contact links rather than leaving them at a dead end. Verified as an unpromoted version on real infrastructure before taking production traffic, since a Turnstile token cannot be produced from an automated browser and this same change broke the form last time. --- src/lib/server/contact.test.ts | 9 ++++----- src/lib/server/contact.ts | 15 ++++++--------- src/routes/contact/+page.server.ts | 3 ++- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/lib/server/contact.test.ts b/src/lib/server/contact.test.ts index 5761392..454abc7 100644 --- a/src/lib/server/contact.test.ts +++ b/src/lib/server/contact.test.ts @@ -132,17 +132,16 @@ describe('turnstile verification', () => { expect(r.reason).toContain('invalid-input-response'); }); - it('allows a submission with no token rather than blocking the form', async () => { + it('rejects a submission with no token, without calling the network', async () => { let called = false; const fetcher = async () => { called = true; return ok(); }; - // A missing token means the widget did not render or was blocked. Failing - // closed here would take the contact form down for anyone running a - // script blocker. + // A bot that never runs JavaScript produces no token at all, so this is + // the case that catches the crudest spam. const r = await verifyTurnstile('', 'sec', undefined, fetcher as typeof fetch); - expect(r.ok).toBe(true); + expect(r.ok).toBe(false); expect(r.reason).toContain('missing'); expect(called).toBe(false); }); diff --git a/src/lib/server/contact.ts b/src/lib/server/contact.ts index 1dfd5f1..1a3b945 100644 --- a/src/lib/server/contact.ts +++ b/src/lib/server/contact.ts @@ -19,15 +19,12 @@ export async function verifyTurnstile( fetcher: typeof fetch, ): Promise<{ ok: boolean; reason?: string }> { if (!secret) return { ok: true }; - // A missing token means the widget never rendered or was blocked, not that - // the sender is a bot. Blocking here takes the whole contact form down for - // anyone running a script blocker, which is worse than the spam it prevents. - // Cloudflare only sees real submissions anyway, and the rate limiter still - // applies. Log it so a widget that silently stops working is visible. - if (!token) { - console.warn('Contact submission had no Turnstile token; allowing it through'); - return { ok: true, reason: 'missing turnstile token' }; - } + // Rejected on purpose. A bot that does not run JavaScript never produces a + // token at all, so letting these through would wave past the crudest and + // most common kind of spam. The cost is that anyone whose blocker eats + // challenges.cloudflare.com cannot send, which is why the form points them + // at another way to reach out. + if (!token) return { ok: false, reason: 'missing turnstile token' }; const body = new URLSearchParams({ secret, response: token }); if (remoteip) body.set('remoteip', remoteip); diff --git a/src/routes/contact/+page.server.ts b/src/routes/contact/+page.server.ts index 72964cb..c94e54e 100644 --- a/src/routes/contact/+page.server.ts +++ b/src/routes/contact/+page.server.ts @@ -47,7 +47,8 @@ export const actions = { console.warn('Turnstile rejected a submission:', turnstile.reason); return fail(400, { success: false, - message: 'Could not verify you are human. Please try again.', + message: + 'Could not verify you are human. Reload the page and try again, or if you block scripts, reach me on one of the links below.', }); }