by clemens (09.02.2023)

Catching (probably all) email SPAM with a really tasty honeypot

We recently launched one of our projects for a customer of ours where onboarding of new users is to be done manually. So there was a requirement that there should be a very simple sign-up form on the landing page where interested users can enter the email and they will then be contacted by the customers support team.

By the way, you should really check the project out. It’s a really novel way for taking notes, organising your knowledge, your ideas and most importantly visualising the often really complex relations between all those arguments. While at the same time keeping it simple with the option to dive deeper if necessary. Anyway, it’s called Logo Dynamic Cards and you can take a closer look at ld-cards.com.

So after implementing the simplest approach: A simple <form> with an ยด` tag we of course ran into problems with bots signing up on mass. Some of those bots even used actually legit email addresses that had been overtaken and then clicked on the obligatory confirmation email we send after signup. We didn’t expect that happening :thinking_face:.

So after some searching we stumbled upon the suggestion to use a honeypot to filter out SPAM, and the version we came up with is working perfectly so far, catching 100% of all SPAM 🙏.

The version we came up with looks like this: On the landing page there is a single <form> with all the inputs that looks like this:

<form>
    <div class="form-group normal">
        <label>Email</label>
        <input class="form-control" name="email" type="text">
        <label>Name</label>
        <input class="form-control" name="name" type="text">
    </div>

    <button type="submit">Submit</button>
</form>

Now when the form is submitted the server will turn around the values for name and email, treating the email as the honeypot value, and using the name value as the email, i.e. something like this in Elixir code:

def form_submit(conn, params) do
    email = params["name"]
    honeypot = params["email"]

    email = if email == nil or email == "", do: "stepped_into@honey.pot", else: email

    MyApp.Leads.new_lead(email, honeypot)

    # send same answer for SPAM and actual users to not give any hints to the bots
    render(conn, "we_will_contact_you_soon.html")
end

And we must of course make sure that actual users only fill out the name field and leaves the email field blank, something that we can achieve easily using some old fashioned css. However note that we did not simply put a class on the elements we want to hide but instead use some pseudo class selectors so that a bot will need to actually understand the css at a deeper level then simply checking if some class is applied directly to an element. The css currently looks like this, but we could obviously make it even more complicated:

.normal.form-group label:nth-child(3),
.normal.form-group input:nth-child(2) {
    display: none;
}

With this setup every bot so far has filled out the email field, and while some are smart enough to not fill out the name field we still know that these are bots because the email field is the honeypot 😄.

Obviously if you actually need the name of the user (we don’t for this app), you’ll have to modify this a bit. If we need to collect the name in the future we plan to simply collect that name only after the user has confirmed the email, so that we can still keep this setup for our honeypot.