Eric / NYC / Milky Way

Visual

Blog Index

Random 5

Stripe and Tap

Multi-tab Websockets

Ruby vs. Ruby

Cookies

Web Accounts

Emoji Bits

Honeypot Bots

Bad Content

JS Data Chutzpah

Sendgrid Ban

Clean URLs

Git

Concerns

Rails UJS

Extending Devise

ENV Variables

See All

Luring Spam Bots into a Honeypot

September 2020

A custom solution using Rails

I made a website with user accounts, and put it online. Within a couple of days, the bots managed to track it down.

Every 20 minutes or so, an account would be created with a username that looked like mBxzKObvCDiWhgt

Bots that create accounts systemically, every 20 minutes, forever, is not good.

  1. They take up server resources and slow down the site for actual humans.
  2. They also mess up the product. When actual humans try to find other users, they shouldn’t have to wade through a mess of bot accounts.

How they get in

They arrive on the Sign Up page, and submit a form. The form was designed to be simple and quick, so there’s minimal friction for new (human) users.

The flip side is that the form is so simple that robots can figure it out. Only the regular stuff is required: username, password, email. Nothing fancy in the way of Captchas.

Captchas

Captcha is short for Completely Automated Public Turing test to tell Computers and Humans Apart. I just double-checked that was correct, because it seems it should probably be CAPTTTTCAHA.

Anyways, what’s important is what they do — attempt to distinguish between humans and robots. Unfortunately this is usually to the detriment of the user experience.

Before you can post something, you have to find a bridge in 9 small, grainy images.

Apparently robots aren’t good at this practice, and turns out, neither am I.

Honeypots

Honeypots are like reverse Captchas. Instead of asking humans to do the work to prove they aren’t a robot, let’s trick the robot into doing some work.

Once we see a user with his (mechanical) finger in the honeypot, we can safely assume we’ve identified a robot.

This works by adding an additional field to a form, that’s hidden from view. A human won’t see this field, and won’t fill it out. A robot doesn’t have eyes, so they just fill out everything in a form that seems important.

When our application receives the form, if the honeypot field is filled out, we have ourselves a robot. In our case, let’s throw out this data, and not sign up mBxzKObvCDiWhgt.

Implementation using Rails

There’s a Rails gem called invisible_captcha that does exactly what we just described, but it didn’t seem to work for the bots we are dealing with at Bounce, so let’s build a custom honeypot.

Sign Up Form

Inside our form, we include this:

<div class="hp">
  <input type="checkbox" value="1" name="hp" id="hp" autocomplete="off" tabindex="-1">
  <label for="hp">Huemans should NOT clik this.</label>
</div>

And the .hp class has these attributes:

.hp{
  display:none;
  position:absolute;
  overflow:hidden;
  width:1px;
  height:1px;
  opacity:0;
}

It’s probably clear from this CSS that this checkbox is hidden many times over.

For the checkbox itself, we set autocomplete="off" so your browser doesn’t get involved with any sort of autocompletion.

We have tabindex="-1" which means that you can’t tab your way into this field.

We also have an invisible label for this invisible checkbox that says “Huemans should NOT clik this.”. We want this label in the rare chance that the checkbox is visible — maybe a user doesn’t load CSS or they have some weird browser add-on that displays all form fields. Or, if the user is using a screenreader — the label will indicate what’s going on.

So why do we spell it weird? This isn’t an exact science, but there’s a chance that bots have been programmed to actually look out for honeypots, so we don’t want our warning message to be easily parsed by a robot.

Registrations Controller

class Users::RegistrationsController < Devise::RegistrationsController

  before_action :find_bot, only: :create

  private

  def find_bot
    return unless params[:hp] == '1'
    head :ok
  end
end

We add a before_action to the create action of our RegistrationsController that checks to see if the checkbox from our form was checked.

The checkbox had a value set of 1; so when checked, we can can expect our params[:hp] to equal 1.

In our find_bot method, when the checkbox is checked (params[:hp]==1), we return head :ok.

This will halt the request cycle, meaning that the actual create action never runs. Additionally, by returning head :ok, we’re returning the status code of 200 to the client, which the robot will interpret as a success.

Conclusion

This custom honeypot seems to work ok for now. We’ve routed our robots into a false sense of success, without cluttering our database with fake accounts. If they become sentient enough to evade this honeypot, I’ll update this post.