Fix: a password manager filled the honeypot and buried the lead - #74
Merged
Conversation
The first real submission to the live intake form was classified as spam. 1Password filled the hidden field -- named `website`, labelled "Website" -- with the site's own URL, and the row was stored with status 'held', which means no notification is sent. The enquiry existed in the database and nobody was told it had arrived. Two faults, and the silent one is the worse of them. The field was a magnet. Password managers match on name, id and label text, and they ignore autocomplete="off"; tabindex="-1" only stops keyboard focus. It is renamed `topic_ref` -- after nothing, since any name containing website, url, address, phone, company, user or email is a target -- and now carries autocomplete="new-password" rather than "off", plus the ignore attributes 1Password, LastPass and Dashlane honour, aria-hidden, and off-screen positioning. "new-password" is the change that matters, and it came from a writeup Rob found. Browsers ignore the "off" value; every vendor implements suppression while someone is choosing a new password, so that value is respected where the obvious one is not. That writeup suggests naming the field `fax_number`, on the grounds that fax is obsolete enough to be ignored. Not taken: Chrome's address autofill has carried a fax field type, and phone and fax sit inside the address group that gets filled together. A name matching no heuristic at all is a stronger guarantee than one that is merely unfashionable. The real fields keep their proper autofill tokens -- name, email and organization. An attorney filling this form should get autofill; only the trap is suppressed. The verdict was too strong. The code asserted that "a person does not fill a hidden field, so a honeypot hit has effectively no false positives", and that is now disproved. Turnstile is the real bot gate, so a honeypot hit on its own no longer holds a submission back: the row records the reason, the notification is still sent, and the subject leads with [flagged] so a filter can act on it. Only a failed Turnstile still holds. Validation now runs on anything being delivered, since a flagged row still has to be a usable enquiry. Both properties carried the identical field, so rootsystem.com's contact form has had this since Track 1 shipped in early August. Separately, db/migrations-bootstrap.sql now guards each row on the schema its migration creates. It asserted all three were applied, and a local database that had 0001 and 0002 but not 0003 was told 0003 was done, which would have skipped it forever. Each insert is now conditional on something only that migration creates, so the file is safe at any point in the sequence and records nothing on an empty database. Verified locally against wrangler dev: a submission with the honeypot filled is stored with spam_reason 'honeypot' and status 'pending' rather than 'held', a clean submission is unaffected, and email_domain is derived correctly in both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53xszP5CdZLXfhRJWsSyv
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.
The first real submission to the live intake form was classified as spam.
1Password filled the hidden field —
name="website", labelled "Website" — with the site's own URL.status='held'means no notification is sent, so the enquiry sat in the database and nobody learned it had arrived. On a lead-generation form that is the worst available failure shape.Both properties carried the identical field, so
rootsystem.com/contacthas had this since Track 1 shipped in early August.Two faults
The field was a magnet. Password managers match on name, id and label text. They ignore
autocomplete="off", andtabindex="-1"only stops keyboard focus.Renamed
topic_ref, and now carryingautocomplete="new-password"— the change that actually matters, from a writeup Rob found. Browsers ignoreoff; every vendor implements suppression while someone is choosing a new password, so that value is respected where the obvious one is not. Plusdata-1p-ignore/data-lpignore/data-form-type,aria-hidden, and off-screen positioning.That writeup suggests
fax_number, on the grounds that fax is obsolete enough to be ignored. Not taken: Chrome's address autofill has carried a fax field type, and phone and fax sit inside the address group filled as a unit. A name matching no heuristic at all beats one that is merely unfashionable.The real fields keep their proper tokens —
name,email,organization. An attorney filling this form should get autofill; only the trap is suppressed.The verdict was too strong. The code asserted that "a person does not fill a hidden field, so a honeypot hit has effectively no false positives." Disproved. Turnstile is the real bot gate, so a honeypot hit alone no longer holds a submission: the row records the reason, the notification is still sent, and the subject leads with
[flagged]so a filter can act on it. Only a failed Turnstile still holds. Validation now runs on anything being delivered, since a flagged row still has to be a usable enquiry.Also
db/migrations-bootstrap.sqlnow guards each row on the schema its migration creates. It asserted all three were applied; a local database with 0001 and 0002 but not 0003 was told 0003 was done, which would have skipped it forever. Each insert is now conditional on something only that migration creates, so the file is safe at any point in the sequence and records nothing on an empty database.Verified locally
wrangler dev, real POSTs:pending(delivered)honeypotexample.compendingNULLbigfirm.comheld(buried)honeypot🤖 Generated with Claude Code
https://claude.ai/code/session_01J53xszP5CdZLXfhRJWsSyv