The honeypot that ate real users: when the browser fills your bot trap
A contact form showed a cheerful “Thank you!” after submitting — and sent nothing. No row in the spreadsheet, no email notification, no POST in the network tab. The form said it worked.
It failed only for some people, sometimes. It worked perfectly in tests, in incognito, and on a fresh browser. It failed for exactly the people most likely to use the site: those with a saved browser profile — name, email, company.
The bug turned out to be a beautiful little collision between a 15-year-old anti-spam technique and a browser convenience feature. Here’s the whole story, because the fix is one line but the lesson is worth more.
What a honeypot is
A honeypot is a classic, JavaScript-free spam trap. You add a form field that humans can’t see — positioned off-screen, aria-hidden, removed from the tab order — and check it on submit:
- A human never sees it, so it stays empty → accept the submission.
- A naive bot parses the raw HTML, finds an input, and fills every field it sees → the honeypot is non-empty → silently drop it.
The “silently” matters: you never tell the bot it failed. You fake success so it can’t tune its way past you. It’s cheap, requires no third-party service, and catches a surprising amount of low-effort spam.
Our field looked like this:
<div aria-hidden="true" style="position:absolute; left:-9999px">
<label>Company
<input type="text" name="company" autocomplete="off" />
</label>
</div>
And the handler:
const honeypot = String(formData.get('company') ?? '').trim();
if (!honeypot) {
// …actually send the message…
} else {
// honeypot filled → looks like a bot → skip the send, fake success
}
That logic is correct for bots. The problem is who else fills the field.
The culprit: browser profile autofill
Every modern browser ships profile autofill — Chrome calls it “Addresses and more.” It stores your name, email, phone, organization/company, postal address, and quietly fills forms it recognises. It matches fields by their name and type attributes: name, email, tel, organization, company, address, postal-code.
Two things people don’t expect:
- It fills hidden fields too. The browser doesn’t care that the input is parked at
left: -9999px. It seesname="company", decides “I know the user’s company,” and fills it. No click, no focus, no visible UI. - It overrides
autocomplete="off"— more on that below.
So for any visitor with a company saved in their browser profile:
- Page loads → browser autofills
name="company"with"Acme Corp". - Visitor fills in the real fields and hits send.
- Handler reads
honeypot = "Acme Corp"→ non-empty. - Handler concludes “bot,” skips the send, and shows “Thank you!”
- Visitor walks away happy. The message never existed.
We caught it red-handed by attaching a throwaway listener in the browser console to dump the form data at submit time:
form.addEventListener('submit', () => {
const fd = new FormData(form);
console.log({
honeypot: fd.get('company'),
email: fd.get('email'),
msgLen: (fd.get('message') || '').length,
});
}, true);
The output:
{ honeypot: "Acme Corp", email: "real@user.example", msgLen: 7 }
honeypot should have been "". The browser had filled our trap for a real human.
”But I set autocomplete=off!”
Right — and it changed nothing. This is the part that surprises everyone.
autocomplete="off" was designed to suppress the browser’s history-based autocomplete: the little dropdown of values you’ve typed into this field before. Browsers deliberately ignore it for profile autofill of recognised data like name, email, and organization.
The reasoning comes straight from the browser vendors: sites — often mistakenly — slap autocomplete="off" on checkout and login forms, and users still want their saved address and contact details filled in. So profile autofill was made to override the attribute. Chromium’s own documentation and the MDN guidance both spell this out: autocomplete="off" is not a reliable way to stop the browser from filling recognised fields.
The lever that actually works is the field name. Autofill triggers on recognised, semantic names. A name the browser has no heuristic for is left untouched.
The fix
Rename the honeypot to something meaningless that no autofill heuristic recognises:
<div aria-hidden="true" style="position:absolute; left:-9999px">
<label>Leave this field empty
<input type="text" name="hp_field" tabindex="-1" autocomplete="off" />
</label>
</div>
const honeypot = String(formData.get('hp_field') ?? '').trim();
hp_field matches no profile category, so the browser leaves it empty — humans pass, naive bots that fill everything still get caught. One renamed attribute, problem gone.
The tempting wrong turn
The instinct is to make the honeypot name more realistic, so bots are more likely to take the bait — organisationUnit, companyName, workEmail. That re-introduces the exact bug. The more “real” the name looks, the more likely the browser’s autofill (and a smarter bot’s field-matcher) treats it as a genuine field and fills it for a human.
The mental model to hold: a honeypot field should be invisible to the browser, not attractive to bots. You’re hiding the trap from autofill — not advertising it.
Why this class of bug is so easy to miss
- It never reproduces in testing. Clean environments, incognito windows, and fresh browsers have no saved profile, so the field stays empty and everything works. It only fails for real users with saved data — and developers rarely test as those users.
- The failure is completely silent. A honeypot’s whole job is to not tip off bots, so the form reports success either way. There’s no error, no log, no exception — just a message that quietly never arrives.
- The obvious safeguard looks like it should work.
autocomplete="off"is sitting right there in the markup, so the field name is the last place anyone thinks to look.
A debugging lesson worth keeping
When a form “succeeds” but nothing lands on the backend, don’t start with the network or the server. Start by dumping the actual FormData the browser is about to send, with a one-off submit listener in the console. The data the browser sends is not always the data the user typed — autofill, extensions, and password managers all reach into forms before you do. Seeing the real payload pointed straight at the filled honeypot in seconds, after a lot of fruitless staring at “why is there no POST?”
Takeaways
- Never give a honeypot field a recognised, semantic name —
company,email,organization,name,tel, or any address field. Browser profile autofill will fill it for real users and your trap will silently drop them. autocomplete="off"does not stop profile autofill. It only governs per-field typing history. The field name is the real control.- Use a meaningless name like
hp_field— invisible to autofill, still effective against naive bots. - When a form succeeds but nothing arrives, inspect the real submitted
FormDatafirst. What the browser sends and what the user typed are not always the same thing.
Comments