PHP: Spam Prevention Using Honeypot Method

When it comes to spam prevention, it is important to find a reasonable balance between efficiency in filtering and overall user experience. Neither a too liberal method of fighting spam nor a form that is painful for a user to submit are the case. Let's be wiser than spammers by not only preventing them from polluting your mailbox but (what's even more satisfying) also letting them think that they have accomplished their task.

The Idea

Regardless of a technical way in which you are going to implement the honeypot trap, the idea is relatively simple and goes as follows.

Within the form you want to protect against spam, put a hidden input element and give it a name so that it looks like a legit field a spammer or bot will unquestionably fill-in. This hidden field is going to be your honeypot. Obtaining this field from the HTTP Request will be a clear indication that it was not submitted by someone you want to receive a message from.

Let's use a typical contact form as an example. At bare minimum we would have two mandatory text inputs like sender's name, sender's e-mail address and obviously a textarea for message content. These types of inputs are obvious for everyone. Specifying a honeypot (trap) input called are_you_a_human or fill_me are too naive and can raise doubts to bots and crawlers who seek for any forms to submit their rubbish content into. How about a low-profile input name like pager_number, fax_number or middle_name?

To picture it, try answering yourself below questions:

  • Do you care these days about your visitor's pager or fax number?
  • Is it necessary for you to know your visitor's middle name?

You get it, right? These fields are redundant yet they look perfectly legit.

Setting up the trap

Define an input field that you do not care to be filled-in by your visitors. Make this field invisible for a human being.

<form action="{{ route('frontend.contact.post') }}" method="post">
    {{ csrf_field() }}

    <input name="name" type="text" value="{{ old('name') }}">
    <input name="middle_name" type="text" value="{{ old('middle_name') }}"> <!-- hide this field and never make it required -->
    <input name="email" type="text" value="{{ old('email') }}">
    <textarea name="message">{{ old('message') }}</textarea>

    <input type="submit" value="Submit">
</form>
Distinguishing a spam

Is it a spam or not? It's a simple yes / no type of question, hence a simple boolean function can be implemented to answer it. If the honeypot field is present in the Request and not empty, then we know with who we are dealing with.

private function isSpam(Request $request): bool
{
    return $request->has('middle_name') && !empty($request->get('middle_name'));
}
The main logic

Below is the logic we execute after a form is submitted.

public function postContactForm(ContactFormRequest $request)
{
    try {
        $isSpam = $this->isSpam($request);

        if (!$isSpam) {
            $this->mailer
                ->to(config('mail.contact.address'))
                ->send(new ContactForm());
        }

        sleep($isSpam ? 8 : 0);

        return redirect()
            ->route('frontend.contact.get')
            ->with('flash-success', 'Message sent successfully.');
    } catch (Exception $e) {
        // something went wrong
    }
}

First we determine if we deal with a legit request or not. If the Reqeust is not a spam then we sent a message immediately. Obviously with more sophisticated approach you can dispatch a message into a queue asynchronously, but for the sake of simplicity we are not going to complicate things here. Finally there are two extremely important chunks of this logic. Follow the last chapter.

Make them think they won!

The sleep() method as well as the successful redirection are crucial to achieve our goal. First one gives a spammer an impression that our backend is doing some heavy lifting with sending their junk message (by delaying the script execution for a bit of time). Second one makes them even more convinced that their submission was successful as they will see exactly what a user with legit request would see - a confirmation that everything went well. There is no reason to let spammers know that we have found out who they are by:

  • redirecting to a 403 page
  • banning their IP address (which almost always is dynamic)
  • making a verbose indication that a spam attempt has been detected in any other way

Make a loser think he is a smartass - this is the key.

Final note

In my humble opinion this method works pretty darn well and does not break UX by forcing your visitors to:

  • fill nasty captchas with letters and digits that are unreadable because of the background noise or ambigious font
  • rotate images with puppies 90° counter clockwise
  • select all the crosswalks among other silly pictures

After all I am quite happy with the solution. It is decent and keeps my mailbox in a great condition.


Last revision 2021-11-23 00:09:50