Sept 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.
- They take up server resources and slow down the site for actual humans.
- 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.