Add abuse protection to the ROI calculator form #24

Merged
JoeBot merged 1 commits from feature/roi-calculator-captcha into main 2026-07-23 07:13:59 -04:00
Owner

The ROI calculator endpoint was open to anyone and has been collecting spam submissions. This adds a layered guard in a new security.js, run before anything is written to the DB or sent to the AI provider.

Layers

  1. Cloudflare Turnstile — real CAPTCHA, verified server-side against siteverify. Active once TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY are set; fails closed if the check can't complete.
  2. Honeypot — hidden hp_company_url field; any value is rejected.
  3. Signed, single-use form token — issued when the modal opens, HMAC-signed, valid 2 hours, enforces a 4s minimum fill time, can't be replayed.
  4. Per-IP rate limit — 5 per 10 minutes, 25 per 24 hours.

Layers 2–4 need no configuration, so spam is throttled as soon as this deploys; adding Turnstile keys upgrades it to a full challenge (invisible for most legitimate visitors).

Blocked submissions now stop the flow client-side and show the reason, instead of silently rendering a result. AI/service errors (502/503) still fall through to the locally-computed estimate.

Also in here

  • trust proxy so client IPs are correct behind nginx
  • 32kb cap on JSON bodies
  • Fixes a latent ReferenceError in the "AI not configured" branch, which called saveCalcSubmission() before the variables it closes over were declared

Verification

Tested against a live server: missing token, instant submit, filled honeypot, tampered signature and token replay were each rejected; a legitimate submission passed; the 6th tripped the 429. Turnstile tested end-to-end against Cloudflare's test keys — always-fail secret → 403, always-pass → 200.

The browser-side flow was not exercised in a real page (Chrome in the dev environment couldn't reach the local port). It passes node --check and was reviewed, but deserves a click-through before deploying.

Deploying

Set in .env (see .env.example):

TURNSTILE_SITE_KEY=...
TURNSTILE_SECRET_KEY=...
FORM_TOKEN_SECRET=$(openssl rand -hex 32)

Not covered

  • /api/leads has no protection and is the same class of target — a one-line guardSubmission call plus the same frontend bits.
  • Rate-limit state is in-process memory, so it resets on restart. Fine for a single instance; would need Redis for multiple.

🤖 Generated with Claude Code

The ROI calculator endpoint was open to anyone and has been collecting spam submissions. This adds a layered guard in a new `security.js`, run before anything is written to the DB or sent to the AI provider. ## Layers 1. **Cloudflare Turnstile** — real CAPTCHA, verified server-side against siteverify. Active once `TURNSTILE_SITE_KEY` / `TURNSTILE_SECRET_KEY` are set; fails closed if the check can't complete. 2. **Honeypot** — hidden `hp_company_url` field; any value is rejected. 3. **Signed, single-use form token** — issued when the modal opens, HMAC-signed, valid 2 hours, enforces a 4s minimum fill time, can't be replayed. 4. **Per-IP rate limit** — 5 per 10 minutes, 25 per 24 hours. Layers 2–4 need no configuration, so spam is throttled as soon as this deploys; adding Turnstile keys upgrades it to a full challenge (invisible for most legitimate visitors). Blocked submissions now stop the flow client-side and show the reason, instead of silently rendering a result. AI/service errors (502/503) still fall through to the locally-computed estimate. ## Also in here - `trust proxy` so client IPs are correct behind nginx - 32kb cap on JSON bodies - Fixes a latent `ReferenceError` in the "AI not configured" branch, which called `saveCalcSubmission()` before the variables it closes over were declared ## Verification Tested against a live server: missing token, instant submit, filled honeypot, tampered signature and token replay were each rejected; a legitimate submission passed; the 6th tripped the 429. Turnstile tested end-to-end against Cloudflare's test keys — always-fail secret → 403, always-pass → 200. The browser-side flow was **not** exercised in a real page (Chrome in the dev environment couldn't reach the local port). It passes `node --check` and was reviewed, but deserves a click-through before deploying. ## Deploying Set in `.env` (see `.env.example`): ``` TURNSTILE_SITE_KEY=... TURNSTILE_SECRET_KEY=... FORM_TOKEN_SECRET=$(openssl rand -hex 32) ``` ## Not covered - `/api/leads` has no protection and is the same class of target — a one-line `guardSubmission` call plus the same frontend bits. - Rate-limit state is in-process memory, so it resets on restart. Fine for a single instance; would need Redis for multiple. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
JoeBot added 1 commit 2026-07-23 07:13:46 -04:00
The calculator endpoint was open to anyone, and it has been collecting
spam submissions. Add a layered guard that runs before anything is
written to the DB or sent to the AI provider:

- Cloudflare Turnstile CAPTCHA, verified server-side (active when
  TURNSTILE_* keys are configured; fails closed if unverifiable)
- Honeypot field that only bots fill in
- Signed, single-use form token enforcing a 4s minimum fill time
- Per-IP rate limiting (5 per 10 min, 25 per 24h)

Layers 2-4 need no configuration and work on their own, so submissions
are throttled immediately; adding Turnstile keys upgrades it to a full
challenge. Blocked submissions now stop the flow client-side instead of
silently showing a result.

Also sets `trust proxy` for correct client IPs behind nginx, caps the
JSON body at 32kb, and fixes a latent ReferenceError in the
"AI not configured" branch that called saveCalcSubmission() before the
variables it closes over were declared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JoeBot merged commit c6425887f1 into main 2026-07-23 07:13:59 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JoeBot/HOALedgerIQ_Website#24
No description provided.