Okay, welcome to 2007.
Let me tell you a story: Our trusty friend-wiki Cheat was getting a lot of spam. Kinda. See, someone suggested I just reject any edits which contained ”<a href”. That worked pretty well, but for some reason the spam-bots kept making empty edits. As in there’d suddenly be 100+ versions of a cheat sheet and they were all identical. The cheat cheat sheet is an example: it has 175 edits of which, like, 4 actually changed anything.
So: the spam bots were having a fun time trying to break through my defenses. Like the raptors in Jurassic Park. Time for drastic measures.
Enter: the Captchator. I have no idea how to pronounce that, really. Sometimes I think I’ve got it but then only a few hours later I lose it again. Anyway, this thing is pretty neat: it’s a captcha web service. You don’t need to install ImageMagick or FreeImage, you just make an image tag with a special captchator.com url and then ask captchator.com if what the user entered in your text box matches what the captcha image actually says.
Like this:
<img src="http://captchator.com/captcha/image/8283"/>
That’ll fetch me a captcha image captchator.com generates. See the 8283? That’s your session id. All I need to do now, on submit, is hit a url like this:
http://captchator.com/captcha/check_answer/8283/peanut
“Peanut” is what the user (read: you) entered into the text box for the captcha check. That url will give me a 1 or a 0 depending on whether ‘peanut’ was really what the captcha generated for session 8283.
Pretty simple. All the spam and empty revisions on Cheat have vanished.
In Ruby?
The Captchator website has all the details, but here’s how I hooked this bad boy up to my lil’ Camping app.
First, the form. The real version is in Markaby, but here it is in rhtml:
<% unless cookies[:passed] %> <% session_id = rand(10_000) %> <img src="http://captchator.com/captcha/image/<%= session_id -%>" /> <input name="chunky" type="hidden" value="<%= session_id -%>" /> <input name="bacon" type="text" size="10" /> <% end %>
(Hey, that’d make a nice little helper method.)
When you answer the Cheat captcha correctly, you get the :passed cookie set. That way you don’t have to keep jumping through hoops. If the cookie’s not set, I grab a random number less than 10,000 and use it for your session id (8283 in the previous example).
The form fields: one, chunky, is your session id. It’s hidden. That way I can check on submit if you’re legit or not. The other, bacon, is what you actually enter as a captcha answer.
Pretty damn simple. In the controller, too:
if captcha_pass? params[:chunky], params[:bacon] cookies[:passed] = true else @error = true end
(That’s a lie. Real code is here, but you get the idea.)
Finally I define my captcha_passed? method, in the controller:
def captcha_pass?(session, answer) session = session.to_i answer = answer.gsub(/\W/, '') open("http://captchator.com/captcha/check_answer/#{session}/#{answer}").read.to_i.nonzero? rescue false end
This will give me back a 1 or a 0 after cleaning our dirty, dirty user input.
And just like that, you’re spam free. Sure, it can be circumvented. But that’s fine. I just want to keep the bots out of my damn campground. We’ve got enough cheaters here, we don’t need no spammers neither.
Alternatives
If you don’t want to use a hosted service, Rob Sanheim has a logic captcha plugin which I’ve played with and enjoy. This Google search pretty much sums up the ballgame, complete with a 100% roll-your-own solution on the Rails wiki.
The Captchator is great for a small site like Cheat because it’s way easy to setup and the images (I think) are pretty easy to read. Andreas (the creator) says he uses it on his Mikrocontroller site so, with any luck, the service will be around for a time to come.
Of course, I’m using Akismet on this here blog and it’s been working wonders. But that’s another story entirely. Ta.
Ha, that rocks. At my last job we built a CAPTCHA service to use for our company’s web sites. And guess what? Yeah, they wanted a SOAP API. So that’s what we built. This is so much simpler. Well, our system had all this crazy stuff for security, but I think a simple URL based API like you have is the way to go.
I find it strange that he doesn’t do more to try and reduce collisions by letting sites register their own ID prefix or something.
Not that there is a huge chance of collisions, but it seems like as the service grows in popularity collisions would become more likely. Maybe (s)he can deal with it then?
Just a little question. When you say “When you answer the Cheat captcha correctly, you get the :passed cookie set.”, I’m interpreting that the cookie between your site and the user gets that :passed item. I think this schema is very simple to forge. Can be possible to set the :passed item in the web session?
josh: I wish this service was in SOAP.
John: I was thinking about that as I wrote this post. “Wait, what if someone reads this post and uses the same random algorithm I use and we have collisions? The horror!” Not a problem yet. Plus, you can always just do “cheat-sheets#{session_id}” or something to namespace your app on your own.
voodmania: It is simple to forge. And guess what, if you look at the Cheat gemsource there’s a simpler way to bypass the CAPTCHA entirely. But I don’t really care. Anyone with motivation could crack this—I just want to keep out the random bots.
Thanks for the post about Captchator.
John: most of the users use a long ID with > 10 characters, generated from the Rails/PHP session ID, which are basically random. The chance of a collision is really negligible.
The implementation can be simplified. If you’re using Rails sessions anyway, you don’t have to create another random number for the captcha and pass it in a hidden field. Just use a part of the session ID, for example @session.session_id[0,15].
As far as I see it this implementation is not secure as the user submits the ID of the used ‘Captchator’. The user could just lookup the right answer/ID combination for one entry and always submit this ID/answer in the HTTP POST.
The ID should not be in the form but in the session.
Jonathan: Try it. The CAPTCHA changes on every load, even for the same session id.
Jonathan: each answer/ID combination can be used only once.
you should use Kernel#open with a block so you io object is closed properly.
to save your pastie code before it vanish def check_captcha! @error ||= !(cookies[:passed] ||= captcha_pass?(params[:chunky], params[:bacon])) end
if you are worried about collisions why not gen a guid or uuid ….
This line was giving me an error:
open(“http://captchator.com/captcha/check_answer/#{session}/#{answer}”).read.to_i.nonzero? rescue false
I added this line to the top of the controller to fix the problem:
require ‘open-uri’
For those who don’t want to use the hosted captcha. Here’s a simple capthcha for Rails
I keep getting a “Bad file descriptor – connect(2)” error when running the line “open(“http://captchator.com/captcha/check_answer/#{session}/#{answer}”).read.to_i.nonzero? rescue false” Does anyone know how to solve this?
Hey, my captcha just isn’t showing up on my site. Actually when I view the source, this:
<% unless cookies[:passed] %> <% session_id = rand(10_000) %> <= session_id ->” /> <% end %>
isn’t even in there. Any ideas? Does the controller code need to be in it’s own controller or the one that is for the form? Thanks
Chime in.