Share one honeypot across the public forms + fix the unchecked bulk payment trap - #2253
Draft
maebeale wants to merge 12 commits into
Draft
Share one honeypot across the public forms + fix the unchecked bulk payment trap#2253maebeale wants to merge 12 commits into
maebeale wants to merge 12 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 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_urlis a real column on bothOrganizationandStory, sogrep website_urlmixes live code with spam traps.website_urlfield 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#createhad 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— oneFIELD_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.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).people.linked_in_urlis 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_rangewas 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
Honeypotunit spec (incl. asserting the name matches no column orFormFieldidentifier), bulk payment honeypot regression.