Skip to content

Share one honeypot across the public forms + fix the unchecked bulk payment trap - #2253

Draft
maebeale wants to merge 12 commits into
mainfrom
maebeale/shared-honeypot
Draft

Share one honeypot across the public forms + fix the unchecked bulk payment trap#2253
maebeale wants to merge 12 commits into
mainfrom
maebeale/shared-honeypot

Conversation

@maebeale

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 3 Read 📖 small guard consolidated across 4 public endpoints, plus one real fix where the guard was missing

Stacked on #2237 — review that first. Base is maebeale/public-form-endpoint, so this diff shows only the honeypot change.

Why

All four public, account-free forms hand-rolled their own decoy input named website_url:

  • website_url is a real column on both Organization and Story, so grep website_url mixes live code with spam traps.
  • Standalone forms are about to get a website_url field identifier — a genuine answer under that name would read as spam.

Raised by @jmilljr24 in review on #2237.

Bug fixed

Bulk payment rendered the decoy but never checked it. Events::BulkPaymentFormSubmissionsController#create had no matching guard, so the trap was decorative and bots filling it were accepted. Added the guard plus a failing-first regression spec.

What

  • Honeypot — one FIELD_NAME (linkedin_profile) + .tripped?(params, scope). Change the name in one place and all four forms follow.
  • shared/_honeypot — one partial, replacing four copies of the markup.
  • All four controllers now call Honeypot.tripped? instead of reading the param by hand.

Name choice

linkedin_profile — these forms never collect a LinkedIn, and it beats a bot's denylist of the usual trap names (website, url, fax).

⚠️ Worth a second opinion: people.linked_in_url is a real column, so this is conceptually adjacent to something the app does store — just never on these forms, and not a literal string collision. budget_range was the runner-up with zero overlap. One-line swap if preferred.

Hiding technique

The shared partial hides off-screen at zero size rather than display: none, which many bots skip. Previously only contact us did this; the other three now get it too.

Scope

Field naming is the weakest anti-bot lever — a scraper that reads the DOM spots opacity-0 / aria-hidden / tabindex="-1" regardless of the name. Rate limiting and a real challenge are tracked separately in #2252.

Tests

  • New: Honeypot unit spec (incl. asserting the name matches no column or FormField identifier), bulk payment honeypot regression.
  • Existing public-form honeypot spec updated to the shared constant.

maebeale and others added 12 commits August 18, 2026 07:38
…c forms

Foundation for a public pretty-URL endpoint for standalone (event-less) forms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…missions index

Publish an event-less Form at /f/:slug for account-free public filling; view
those submissions via the form-filterable /form_submissions index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ons in index

Adds a dev seed (two published event-less forms with submissions/answers) and
marks event-less public submissions with a 'Public form' pill in the index so a
blank Event column reads as intentional, not missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nce onto FormSubmission; forms UX polish

- Bug: View link inside the submissions results Turbo frame navigated the frame
  to a frameless show page (Oopsie); break it out with turbo_frame _top.
- Move persist_answer + hardened file-upload handling onto FormSubmission;
  registration, public forms, and bulk payment now share it (mixin removed).
- Forms index: event-connected unpublished forms read 'Event form' not
  'Not published'; delete link removed; submissions count links back with a
  Forms eyebrow anchored to the row.
- Editor: clarify publish only controls the public link; swap Save/Delete
  placement and show a reason when a form can't be deleted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rt, forged upload)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror event registration: new form_submission_confirmation(_fyi) notification
kinds, NotificationMailer methods + views, job dispatch, and previews. Sent from
PublicFormSubmission to the submitter and the AWBW team.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A form can be both; render a chip for each that applies (rename column to
Availability) rather than one-or-the-other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt SearchFormHelper field/label classes + shared/_search_clear (introduced on
main in #2220) so the form filter matches every other index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssions

Public forms were the only submission path that skipped
OtherResponses::CaptureFromSubmission, so a sector question answered
"Other: …" never reached the curation queue — it only lived in the form
answer, invisible to promotion.

The submissions View link carried person_id but not form_id, so arriving
from the forms index and clicking back landed on that person's
submissions rather than the form-filtered list you came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four public, account-free forms each hand-rolled a decoy input named
website_url. That name is a real column on both Organization and Story, so
grepping it mixes live code with spam traps, and a future FormField
identifier by that name would make a genuine answer read as spam. Name it
once, for something these forms never collect.

Bulk payment rendered the decoy but its controller never checked it, so the
trap was decorative there and bots filling it were accepted.

The shared partial hides the field off-screen at zero size rather than with
display: none, which many bots skip — previously only contact us did this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Base automatically changed from maebeale/public-form-endpoint to main August 18, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant