3.7. CAPTCHA

The other commonly used mechanism for preventing spambots from taking over your system is those blurry, transmogrified letters and numbers. The generic name for those things is CAPTCHA, which stands for Completely Automated Public Turing test to tell Computers and Humans Apart (which is not only one of the most tortured acronyms you'll ever see, but is, according to Wikipedia, a registered trademark of Carnegie Mellon University).

Now, I am of two minds about the familiar CAPTCHA images. On the one hand, it's true that a good implementation is difficult, if not impossible, for bots to crack. On the other hand, CAPTCHA images are not at all accessible to visually impaired users, which under certain circumstances might have legal consequences for your site. Even for users with normal sight, these images can still be awkward and are somewhat mistake-prone. In addition, users hate them.

What I'm going to do is present a simple CAPTCHA system that presents a text-based addition problem for the user to solve, such as "What is three plus the number of days in a week plus the number of fingers on a hand?" I'll leave it up to you to decide whether that is more or less irritating to a user than a fuzzy image. I'm pretty sure, though, that it will be more usable for a visually impaired user. It will use the existing token mechanism to store and validate user input. It's not a full-protection CAPTCHA — in fact, according to the somewhat sneering tone of the Wikipedia article, it's not a true CAPTCHA at all — but it should keep the riff-raff out, and it's a mechanism for discussing the issues involved. Later, I'll point out a few existing Rails plugins that can do more traditional CAPTCHA, if you expect to have determined spammers targeting your site.

3.7.1. Creating a Test-Driven CAPTCHA Object

The implementation of the simple CAPTCHA will be a Ruby class, MathCaptcha, that you can place in app/models/math_captcha.rb. You will also add a test/unit/math_captcha_test.rb file to the unit test directory. The functionality of the CAPTCHA object is pretty basic — it needs to generate a random sequence of operands, determine their sum, and convert them to a string for output.

Some Rails developers think that applications and models should be strictly limited to ActiveRecord subclasses, in which case, the MathCaptcha object could be placed either in the lib directory or in a new application subdirectory such as app/utilities. Alternately, the MathCaptcha could be developed as a plugin. (See Chapter 15 for more details.)

You'll need to place three unit tests in the math_capcha_test.rb file. The first test generates a list of operands:

require File.dirname(__FILE__) + '/../test_helper'

class MathCaptchaTest < Test::Unit::TestCase

  def setup
    @captcha = MathCaptcha.new
  end

  def test_should_generate_list
    @captcha.generate_operands(3)
    assert_equal 3, @captcha.operands.size
    @captcha.random_stream = [1, 3, 5]
    @captcha.generate_operands(3)
    assert_equal [1, 3, 5], @captcha.operands
    assert_equal 9, @captcha.sum
  end

The only unusual thing about this test is that I chose to give the actual CAPTCHA object a random_stream attribute that allows me to inject fake-random numbers to the object to test the result of a particular sequence of random numbers on the object. I find this mechanism to be much more manageable than using srand to specify the random number seed. Another option would be to split the functionality into two methods: one method that generates a random number and then calls the other method with that argument, and then in the unit tests, you call the second method with a prepared argument. In any case, I recommend that you have some way to specify random inputs for testing.

Test two verifies that the CAPTCHA can generate multiple string representations of the same operation, as follows:

def test_string_representation
    assert_equal (["7", "seven", "the number of days in a week"],
        MathCaptcha::OPTIONS[7])
    @captcha.random_stream = [1, 3, 5, 0, 0, 0]
    @captcha.generate_operands(3)
    assert_equal "What is 1 plus 3 plus 5?", @captcha.display_string
    @captcha.random_stream = [1, 3, 5, 1, 1, 1]
    @captcha.generate_operands(3)
    assert_equal "What is one plus three plus five?",
        @captcha.display_string
  end

Test three verifies that the CAPTCHA generates a token object and places it in the database, like this:

def test_token
    @captcha.random_stream = [1, 3, 5, 1, 1, 1]
    @captcha.generate_operands(3)
    assert_difference('Token.count') do

@captcha.generate_token
      assert_equal 9, @captcha.token.value
    end
  end

end

3.7.2. CAPTCHA Object Implementation

The MathCaptcha class will have three attributes: operands, which will represent the actual list of numbers to be added; random_stream, which will house the random numbers and be used for testing; and token, which will hold the token object associated with this CAPTCHA. The skeleton of the class looks like this:

require 'extensions'
class MathCaptcha
  attr_accessor :operands, :random_stream, :token
end

I suppose it should go without saying, but the remaining methods in this section will go inside that class definition. I just think that it will be easier to explain the code piece by piece rather than in a larger code dump.

The first unit test involves generating a random list of attributes and knowing their correct sum:

def initialize
    self.random_stream = []
  end

  def generate_operands(size)
    self.operands = (1..size).collect { get_random }
  end

  def get_random(max=21)
    result = random_stream.shift
    if result then result else rand(max) end
  end

To generate the operands, I used the same range and collection trick previously used for the randomized token strings — this method allows for an arbitrary length of math sentence, although I suppose using less than two operands is kind of pointless. The get_random method is the hook for preset random streams. If a preset array exists, then the first value in that array is taken and used; otherwise, the system random function is used. The operands are random values between 0 and 20.

The sum of the operands is a simple one-line call to the already generated array:

def sum
    operands.sum
  end

The second test involves turning the math sentence into a string. The data structure here is a class constant called OPTIONS, a section of which looks like this:

OPTIONS = [
    ["0", "zero", "nothing", "the number of legs on a fish"],
    ["1", "one", "the number of thumbs in a hand"],
    ["2", "two", "the number of feet a person has"],
    ["3", "three", "the number of sides in a triangle"],

I trust you get the idea. To create the display string, the object chooses one of the synonyms for each number at random, and pieces them together into a sentence, like this:

def display_string
    operand_string = operands.collect { |o|
       string_for_operand(o) }.join(" plus ")
    "What is #{operand_string}?"
  end

  def string_for_operand(operand)
    opts = OPTIONS[operand]
    opts[get_random(opts.size)]
  end

As is so often the case when you're manipulating lists, the collect-and-join combination is your friend. It enables the array to be converted to the data string in a single line of code that can then be slotted in the sentence skeleton. Also notice that the string_for_operand method uses the same get_random hook, which allows the unit test to validate specific combinations of the number options.

Finally, the CAPTCHA object needs to create a unique token. You want to do this because one of the ways in which CAPTCHA systems are subverted is by breaking the code on either the filename used for the image or a hidden field used within the form to identify the CAPTCHA being used. Because this CAPTCHA will use the token mechanism to create a one-off random string unrelated to the value of the CAPTCHA, that line of attack should be defended against. The token is defined as follows:

def generate_token
    self.token = Token.create(:value => sum,
        :token => Standards.random_string(25),
        :expires_at => DateTime.now + 2)
  end

Of course, this is not really an industrial-strength CAPTCHA, and it's not out of the question that a determined spammer could parse the string. The most likely weakness of this system, however, is probably the fact that there are relatively few possible answers — because you'll use this with three operands, there are only 61 possibilities. A mechanism could easily cycle through them on the same CAPTCHA, or could just guess and accept the 1.6-percent success rate. That would require a spammer to be deliberately targeting your site, of course. Some of these vulnerabilities can be mitigated during deployment by restricting repeated access from the same site.

It's not part of the test, but you will need a constructor for this object to use during deployment. It should create the operands and generate the token, as follows:

def self.create(operand_count, random_stream = [])
    result = MathCaptcha.new
    result.random_stream = random_stream
    result.generate_operands(operand_count)
    result.generate_token
    result
  end

I named the method create rather than new, to conform to the Rails convention that create is used to denote class methods that save something to the database — in this case, the token. At the moment, there's no need for a specific method that would just generate the operands and tokens and would not save the tokens to the database.

3.7.3. Deploying the CAPTCHA

The CAPTCHA needs to be deployed as part of the new user form and validated on user creation. It's probably also a good idea to put it in the recipe form and validate new recipe creation, lest spammers get one legitimate login and pummel the site with recipes for "Get Rich Quick with Penny Stocks Soup." The testing and implementation of the two are nearly parallel, so I'll only show the recipe side here.

I placed the following two common testing methods in test_helper.rb:

def create_mock_captcha_token(token, value)
    Token.create(:token => token, :value => value,
        :expires_at => Date.today + 2)
  end

  def assert_captcha
    created_token = assigns(:captcha).token
    saved_token = Token.find_by_value(assigns(:captcha).sum)
    assert_equal created_token.token, saved_token.token
  end

The first method creates a fake token for a mythical CAPTCHA so that user input can be validated against it. The second method asserts that a token has been created by a CAPTCHA object, and that it has the expected value.

Now the actual tests need to be created. The test for new needs to validate that the CAPTCHA is created and that the form elements are placed in the form. This needs to be placed in recipes_controller_test.rb, with the new lines highlighted here:

def test_should_get_new
    get :new
    assert_response :success
    assert_captcha
    assert_select("form[action=?]", recipes_path) do

assert_select "input[name *= title]"
      assert_select "input[name *= servings]"
      assert_select "textarea[name *= ingredient_string]"
      assert_select "textarea[name *= description]"
      assert_select "textarea[name *= directions]"
      assert_select "input[name *= captcha_value]"
      assert_select "input[name *= token]"
    end
  end

The test for create needs to add the valid CAPTCHA data, and a new test needs to be added to test correct behavior when invalid CAPTCHA data is presented. The existing create test has new lines that are highlighted in the following code:

def test_should_create_recipe
    create_mock_captcha_token("fred", "3")
    recipe_hash = { :title => "Grandma's Chicken Soup",
        :servings => "5 to 7",
        :description => "Good for what ails you",
        :ingredient_string =>
            "2 cups carrots, diced

1/2 tablespoon salt


             1 1/3 cups stock",
        :directions => "Ask Grandma"}
    assert_difference('Recipe.count') do
      post :create, :recipe => recipe_hash, :token => "fred",
           :captcha_value => "3"
    end
    expected_recipe = Recipe.new(recipe_hash)
    new_recipe = Recipe.find(:all, :order => "id DESC", :limit => 1)[0]
    assert_equal(expected_recipe, new_recipe)
    assert_equal(3, new_recipe.ingredients.size)
    assert_redirected_to recipe_path(assigns(:recipe))
  end

And the new test looks like this:

def test_should_not_create_recipe_without_captcha
    create_mock_captcha_token("fred", "3")
    old_size = Recipe.count
    recipe_hash = { :title => "Grandma's Chicken Soup",
        :servings => "5 to 7",
        :description => "Good for what ails you",
        :ingredient_string => "2 cups carrots, diced",
        :directions => "Ask Grandma"}
    post :create, :recipe => recipe_hash, :token => "fred",
          :captcha_value => "5"
    new_size = Recipe.count
    assert_equal old_size, new_size
  end

Passing these tests involves slight changes to the new and create methods of the recipes controller to use the CAPTCHA data that you have already created. The change to new is as simple adding the highlighted line in the following code):

def new
    @recipe = Recipe.new
    @captcha = MathCaptcha.create(3)
    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @recipe }
    end
  end

The create change is a little more involved (but only a little). You check to see if the CAPTCHA is valid before allowing the new recipe to be in the database. Otherwise, the code redirects back to the form — but because @recipe has the user-entered data in it, the user will not have to reenter all the recipe data. Note the following highlighted code:

def create
    @recipe = Recipe.new(params[:recipe])
    if Token.is_valid_captcha(params[:token], params[:captcha_value])
      saved = @recipe.save
    else
      @recipe.destroy
      flash[:notice] = 'Sorry, you answered the robot question
              incorrectly, please try again.'
      saved = false
    end
    respond_to do |format|
      if saved
        flash[:notice] = 'Recipe was successfully created.'
        format.html { redirect_to(@recipe) }
        format.xml  { render :xml => @recipe, :status => :created, :location =>  @recipe }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @recipe.errors, :status => :unprocessable_entity }
      end
    end
  end

This code depends on the following method of the Token class in app/models/token.rb to determine if the token and the incoming CAPTCHA value really do match:

def self.is_valid_captcha(token, incoming_value)
    actual_token = Token.find_by_token(token)
    return false unless actual_token
    actual_token.destroy
    actual_token.is_valid?(token, incoming_value)
  end

This method finds the token by its random token value and asserts that the incoming value matches the calculated value stored with the token. No matter what happens, the token is removed from the database, so the same token cannot be exploited twice.

The user's controller has extremely similar changes.

I also created a view to display the CAPTCHA inside a form block. I placed it in app/views/math_captcha/_math_captcha.html.erb. It puts the token in a hidden field, and gives the user a space to enter his answer, as follows:

<tr>
  <td colspan="2">
    We regret the inconvenience, but we need you to answer the
    following question to prove that you are not a robot.  </td>
</tr>
<input type="hidden" name="token" value="<%= @captcha.token.token %>">
<tr>
  <td colspan="2">
    <%= @captcha.display_string %>
  </td>
</tr>
<tr>
  <td class="tdheader">Answer (as digits):</td>
  <td>
    <input class="input" type="text" name="captcha_value" size="5">
  </td>
</tr>

The fields are inside table elements to allow them to coexist with the tabular form builder used in the user form. To insert this partial template in the new recipes form, it just needs to have the following table around it:

<% if @captcha %>
    <table>
      <%= render :partial => "math_captcha/math_captcha" %>
    </table>
  <% end %>

The end product puts the new form elements at the bottom of each form, as shown in Figure 3-4.

Figure 3.4. Figure 3-4

That's not quite the end of the story. There are some more elaborate features on the server side that can help security. You can keep track of how long it takes the user to fill out the form, and only accept forms that are completed within a certain length of time, such as 30 seconds. You can block a given requester IP from performing more than a few CAPTCHA checks in a certain amount of time.

If you want the next level of protection, there are some image-based CAPTCHA plugins for Rails (which you can find at the websites listed in the "Resources" section later in this chapter).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.217.41