Skip to content

Fix: a password manager filled the honeypot and buried the lead - #74

Merged
robbyrob42 merged 1 commit into
primaryfrom
rob/honeypot-false-positive
Sep 3, 2026
Merged

Fix: a password manager filled the honeypot and buried the lead#74
robbyrob42 merged 1 commit into
primaryfrom
rob/honeypot-false-positive

Conversation

@robbyrob42

Copy link
Copy Markdown
Collaborator

The first real submission to the live intake form was classified as spam.

status        held
spam_reason   honeypot
spam_detail   honeypot: https://www.rootsystem.com

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/contact has 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", and tabindex="-1" only stops keyboard focus.

Renamed topic_ref, and now carrying autocomplete="new-password" — the change that actually matters, from a writeup Rob found. Browsers ignore off; every vendor implements suppression while someone is choosing a new password, so that value is respected where the obvious one is not. Plus data-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.sql now 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:

Submission status spam_reason email_domain
honeypot filled pending (delivered) honeypot example.com
clean pending NULL bigfirm.com
(old behavior) held (buried) honeypot

🤖 Generated with Claude Code

https://claude.ai/code/session_01J53xszP5CdZLXfhRJWsSyv

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
@robbyrob42
robbyrob42 merged commit ab44ac0 into primary Sep 3, 2026
4 checks passed
@robbyrob42
robbyrob42 deleted the rob/honeypot-false-positive branch September 3, 2026 23:37
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