How to Prevent Bot Spam on Your Ruby on Rails Website
Tags: ruby, rails, webdev, tutorial
Published: 2022-10-21 15:08:37

If you have an unprotected form on your website, bots are going to spam you.

That's just how it works.

Luckily, it's not hard at all to protect your forms and prevent this from happening.

If you <a href="https://testsuite.io/detect-bot-form-submissions">detect bot spam</a>, there are 2 strategies that I've found most effective:

- Using a honeypot
- Using a captcha tool

A <a href="https://en.wikipedia.org/wiki/Honeypot_(computing)">honeypot</a> is essentially a trap for a bot, a fake field or key piece of information that gives away the fact that a human didn't submit that form. For example, an invisible field. If it was filled out, it was very likely not done by a human.

Otherwise, you can rely on <a href="https://www.google.com/recaptcha/about/">Google reCAPTCHA</a>. But there are a few major drawbacks here. The first is performance, your site will be slower to load since it has to load Recaptcha scripts. And the second is the user experience, people generally hate having to do captchas, "click on all the sidewalks", those kinds of things.

So ultimately I'm a bigger fan of the honeypot strategy, PLUS is way easier to build into a Rails app.

---

## How to Create a Honeypot in Ruby on Rails

A honeypot is a trap, something that a human can easily distinguish but a bot cannot.

Take for example a form with fields for email and "leave this empty". A bot would likely not leave that field empty because it would not be able to understand the meaning of that label. A human would be much better at following directions and not inputting anything in that field.

Boom you have a honeypot - granted this one is pretty bad and not a great user experience. 

But hopefully, it outlines how easy it can be to build.

#### BUILDING THE HONEYPOT FRONTEND

Let's build off the above example but make it better.

Here's a breakdown of the honeypot that this website uses. (And since enabling it we have not received a single bot submission on any of our forms.)

We start with a normal form, we use `form_with` but it doesn't matter how you build your form.

All we have to do is add a field that should not be filled out, in this case a "hidden message":

```erb
<%= form.email_field :hidden_message, placeholder: 'Message', class: 'hidden-message' %>
```

We give it the CSS class `hidden-message` so that we can hide it on the page, but a bot that's just parsing our HTML will still find it and fill it out.

The `hidden-message` class is styled like this:

```css
.hidden-message {
  // honeypot form field
  opacity: 0;
  position: absolute;
  top: 0;
  left: 0;
  height: 0;
  width: 0;
  z-index: -1;
}
```

This ensures it has 0 opacity, position absolute so it doesn't shift anything else in the DOM, and has no dimensions so it cannot be clicked on.

We simply place this field into our form and are done with the frontend! (that was one line of code plus some optional CSS)

```erb
<%= form_with url: emails_index_path, local: true, id: 'email-form', class: 'form' do |form| %>
  <div class='field'>
    <%= form.email_field :hidden_message, placeholder: 'Message', class: 'hidden-message' %>
    <%= form.email_field :email, placeholder: 'Enter your email', class: 'input' %>
  </div>
  <%#= form.submit 'Join', class: 'button' %>
<% end %>
```

#### BUILDING THE HONEYPOT BACKEND

Doing anything on the backend is optional now since you can simply filter out your bots by checking to see if the hidden message is filled out or not, but to make things easier you can avoid spam from even being processed and saved in your system.

We have an endpoint that our email form points to which processes and saves subscribers.

In order to filter out bot submissions, we just check for the presence of our honeypot field.

Something like this:

```ruby
redirect_to request.referrer if params[:hidden_message].present?
```

What that looks like inside of our form submission endpoint:

```ruby
  def emails
    if params[:hidden_message].present?
      redirect_to request.referrer
    else
      # Logic to handle the form submission
      Email.create(email: params[:email])
      redirect_to request.referrer
    end
  end
```

And that's it!

By taking an existing Rails form, we added a field and styled it so it was hidden and on the backend redirect you if you filled out the honeypot.


---

## Important Points

I prefer the honeypot strategy because it's much more performant, no fancy scripts to load or requests that affect your website's pageload speed. (Pageload has a big impact on user experience but also your SEO - it's important.)

But do keep in mind, a honeypot is not perfect.

First, is the issue with _accessibility_.

We literally created a field and visually hid it, meaning anyone using a screen reader or other tools will still come across it just as a bot would. This means it's important to name your field clearly and instruct the reader to not fill it out. A bot is going to be less likely to process these instructions correctly.

And of course, if you are dealing with a direct attack (which is unlikely) the attacker could certainly add to their script and have the bot ignore your honeypot field.

But the alternative is pulling in scripts and external third-party tools that will impact your site performance, user experience, and overall traffic so the decision is pretty clear to me!

Good luck in your fight against the bots (and let's hope AI doesn't make this even harder on us in a few years).