Captcha (keeping those spammers out of your “business”) Part 03: Adding a user interface to the captcha

December 21, 2007

In Part 03 of this series of posts we are going to build a simple input form to hold the captcha image.

First, I will write out some HTML and in the <body> I will include a simple form.

<form method="post" action="" id="comment_form">
  <ul>
    <li>
      <label for="form_name">Name</label>
      <input type="text" name="form_name" id="form_name" />
    </li>
    <li>
      <label for="form_email">Email</label>
      <input type="text" name="form_email" id="form_email" />
    </li>
    <li>
      <label for="form_captcha"> </label>
      <img id="form_captcha" />
    </li>
    <li>
      <label for="form_captcha_instructions"> </label>
      <span id="form_captcha_instructions">enter the code exactly as shown above</span>
    </li>
    <li>
      <label for="form_captcha_value">Code<em>*</em></label>
      <input type="text" name="form_captcha_value" id="form_captcha_value" />
    </li>
    <li>
      <label for="form_comment">Comment<em>*</em></label>
      <textarea id="form_comment" name="form_comment" rows="10"></textarea>
    </li>
    <li>
      <label for="submit"> </label>
      <input type="button" name="submit" value="Add Comment" id="submit"/>
    </li>
  </ul>
</form>

Curious to see how horrible this looks? Below is a snapshot:

Captcha Form, No Style — Literally

Before we continue with the Javascript, I will add some CSS to cleanup the layout:

      *
      {
        font: 10pt Arial;
        margin: 0;
        padding: 0;
      }
      #comment_form
      {
        margin: 5px;
        padding: 5px;
        border: 1px Solid #e5e5e5;
        width: 400px;
        background: #f9f9f9;
      }
        #comment_form ul
        {
          list-style: none;
        }
        #comment_form ul li
        {
          margin-bottom: 5px;
        }
            #comment_form ul li label
            {
              width: 75px;
              float: left;
            }
            #comment_form ul li textarea
            {
              border: 1px Solid #e5e5e5;
              width: 313px;
            }
            #comment_form ul li input
            {
              border: 1px Solid #e5e5e5;
              width: 313px;
            }
              #comment_form ul li label em
              {
                color: red;
              }

There, some simple CSS and things are starting to look a lot better.

Captcha Form, with some style

To include the captcha image within the form I will use some javascript to call the captcha.php. This is easily accomplished by using some very simple Javascript to modify the DOM.

index.php cont…

      function getCaptchaImage(session_id)
      {
        document.getElementById("form_captcha").src="captcha.php?action=challenge&session_id=" + session_id;
      }

The getCaptchaImage() function adds the ‘src’ attribute to the <img> in our HTML. We can call this function in the <body> by using <body onLoad=”getCaptchaImage(session_id);”>. But the session_id value will need to be determined by some PHP.

Up to this point, here is the index.php:

index.php

<?php
session_start();
?>
<html>
  <head>
    <style type="text/css">
      *
      {
        font: 10pt Arial;
        margin: 0;
        padding: 0;
      }
      #comment_form
      {
        margin: 5px;
        padding: 5px;
        border: 1px Solid #e5e5e5;
        width: 400px;
        background: #f9f9f9;
      }
        #comment_form ul
        {
          list-style: none;
        }
        #comment_form ul li
        {
          margin-bottom: 5px;
        }
            #comment_form ul li label
            {
              width: 75px;
              float: left;
            }
            #comment_form ul li textarea
            {
              border: 1px Solid #e5e5e5;
              width: 313px;
            }
            #comment_form ul li input
            {
              border: 1px Solid #e5e5e5;
              width: 313px;
            }
              #comment_form ul li label em
              {
                color: red;
              }
    </style>
    <script language="Javascript">
      function getCaptchaImage(session_id)
      {
        document.getElementById("form_captcha").src="captcha.php?action=challenge&session_id=" + session_id;
      }
    </script>
  </head>
<body onLoad="getCaptchaImage('<?php echo session_id();?>');">
<form method="post" action="" id="comment_form">
  <ul>
    <li>
      <label for="form_name">Name</label>
      <input type="text" name="form_name" id="form_name" />
    </li>
    <li>
      <label for="form_email">Email</label>
      <input type="text" name="form_email" id="form_email" />
    </li>
    <li>
      <label for="form_captcha"> </label>
      <img id="form_captcha" />
    </li>
    <li>
      <label for="form_captcha_instructions"> </label>
      <span id="form_captcha_instructions">enter the code exactly as shown above</span>
    </li>
    <li>
      <label for="form_captcha_value">Code<em>*</em></label>
      <input type="text" name="form_captcha_value" id="form_captcha_value" />
    </li>
    <li>
      <label for="form_comment">Comment<em>*</em></label>
      <textarea id="form_comment" name="form_comment" rows="10"></textarea>
    </li>
    <li>
      <label for="submit"> </label>
      <input type="button" name="submit" value="Add Comment" id="submit" />
    </li>
  </ul>
</form>
</body>
</html>

And our result:

Captcha Form, dynamically loading the captcha

A quick check of our database verifies that the correct captcha values have been saved to the database (our PHP is working!) :

mysql> select * from captcha;
+----------------------------+-----------+
| sid                        | challenge |
+----------------------------+-----------+
| upgf36fgm8o9b1a381fmk90gf4 | 406fb     |
+----------------------------+-----------+
1 row in set (0.00 sec)

Now we need a way to determine if the user has entered the correct captcha code to the input box. The check will be completed when the user clicks the “Add Comment” button. By using some Ajax we can easily determine if the code is correct.

index.php cont…

      function createRequestObject()
      {
        var reqObj = null;
        //Attempt to create native XMLHttpRequest Object
        if(window.XMLHttpRequest)
        {
          try
          {
            reqObj = new XMLHttpRequest();
          }
          catch(error)
          {
            reqObj = null;
          }
        }
        //Attempt to create ActiveX version
        else if(window.ActiveXObject)
        {
          try
          {
            reqObj = new ActiveXObject("Msxml2.XMLHTTP");
          }
          catch(error)
          {
            try
            {
              reqObj = new ActiveXObject("Microsoft.XMLHTTP");
            }
            catch(error)
            {
              reqObj = null;
            }
          }
        }
        return reqObj;
      }

      var ro = createRequestObject();

      function testChallenge(sid)
      {
        var challenge = document.getElementById('form_captcha_value').value;
        ro.open('get', 'captcha.php?session_id=' + sid + '&action=verify&challenge=' + challenge, true);
        ro.onreadystatechange = getChallengeResponse;
        ro.send(null);
      }

      function getChallengeResponse()
      {
        if(ro.readyState == 4)
        {
          alert(ro.responseText);
        }
      }

In the <script> I have added 3 new functions and 1 variable. The createRequestObject() function is responsible for creating our repsonse object based on the user’s browser type (IE or a browser that natively support XMLHttpRequest). The testChallenge() function sends the request to our captcha.php and waits for a response. The getChallengeResponse() will display an alert with the response from our captcha.php when it has completed processing on the server.

Now to trigger the testChallenge() function we will modify the “Add Comment” button.

index.php complete

<?php
session_start();
?>
<html>
  <head>
    <style type="text/css">
      *
      {
        font: 10pt Arial;
        margin: 0;
        padding: 0;
      }
      #comment_form
      {
        margin: 5px;
        padding: 5px;
        border: 1px Solid #e5e5e5;
        width: 400px;
        background: #f9f9f9;
      }
        #comment_form ul
        {
          list-style: none;
        }
        #comment_form ul li
        {
          margin-bottom: 5px;
        }
            #comment_form ul li label
            {
              width: 75px;
              float: left;
            }
            #comment_form ul li textarea
            {
              border: 1px Solid #e5e5e5;
              width: 313px;
            }
            #comment_form ul li input
            {
              border: 1px Solid #e5e5e5;
              width: 313px;
            }
              #comment_form ul li label em
              {
                color: red;
              }
    </style>
    <script language="Javascript">
      function createRequestObject()
      {
        var reqObj = null;
        //Attempt to create native XMLHttpRequest Object
        if(window.XMLHttpRequest)
        {
          try
          {
            reqObj = new XMLHttpRequest();
          }
          catch(error)
          {
            reqObj = null;
          }
        }
        //Attempt to create ActiveX version
        else if(window.ActiveXObject)
        {
          try
          {
            reqObj = new ActiveXObject("Msxml2.XMLHTTP");
          }
          catch(error)
          {
            try
            {
              reqObj = new ActiveXObject("Microsoft.XMLHTTP");
            }
            catch(error)
            {
              reqObj = null;
            }
          }
        }
        return reqObj;
      }

      var ro = createRequestObject();

      function testChallenge(sid)
      {
        var challenge = document.getElementById('form_captcha_value').value;
        ro.open('get', 'captcha.php?session_id=' + sid + '&action=verify&challenge=' + challenge, true);
        ro.onreadystatechange = getChallengeResponse;
        ro.send(null);
      }

      function getChallengeResponse()
      {
        if(ro.readyState == 4)
        {
          alert(ro.responseText);
        }
      }  

      function getCaptchaImage(session_id)
      {
        document.getElementById("form_captcha").src="captcha.php?action=challenge&session_id=" + session_id;
      }
    </script>
  </head>
  <body onLoad="getCaptchaImage('<?php echo session_id();?>');">
  <form method="post" action="" id="comment_form">
    <ul>
      <li>
        <label for="form_name">Name</label>
        <input type="text" name="form_name" id="form_name" />
      </li>
      <li>
        <label for="form_email">Email</label>
        <input type="text" name="form_email" id="form_email" />
      </li>
      <li>
        <label for="form_captcha"> </label>
        <img id="form_captcha" />
      </li>
      <li>
        <label for="form_captcha_instructions"> </label>
        <span id="form_captcha_instructions">enter the code exactly as shown above</span>
      </li>
      <li>
        <label for="form_captcha_value">Code<em>*</em></label>
        <input type="text" name="form_captcha_value" id="form_captcha_value" />
      </li>
      <li>
        <label for="form_comment">Comment<em>*</em></label>
        <textarea id="form_comment" name="form_comment" rows="10"></textarea>
      </li>
      <li>
        <label for="submit"> </label>
        <input type="button" name="submit" value="Add Comment" id="submit" onClick="testChallenge('<?php echo session_id(); ?>');"/>
      </li>
    </ul>
  </form>
  </body>
</html>

Now lets refresh our index.php and run it from the top.

Step 1: Viewing the form

Captcha Form, Running the Scripts from the top

Step 2: Verify the database has the correct challenge code

mysql> select * from captcha;
+----------------------------+-----------+
| sid                        | challenge |
+----------------------------+-----------+
| upgf36fgm8o9b1a381fmk90gf4 | 8d2ac     |
+----------------------------+-----------+
1 row in set (0.00 sec)

Step 3: Adding the correct captcha challenge code into the input box and the result after clicking “Add Comment”

Captcha Form, Running the Scripts from the top 2

Step 4: Adding the incorrect captcha challenge code into the input box and the result after clicking “Add Comment”

Captcha Form, Running the Scripts from the top 3

Everything is working just the way it was expected too. Now that we have the fundamentals of building a simple captcha method there are some additional issues that require future planning:

  1. Accessibility is a large concern with this captcha as it currently sits. Users with visual impairments may not be able to use this method to access your site. How can we add to our script to ensure all potential users are able to utilize our sites?
  2. I would not consider this captcha to be all that strong. More advanced captcha methods would utilize more sophisticated methods to prevent OCR bots from breaking our spam prevention.

This series of posts was not intended to be an absolute guide to developing captcha. The purpose of this post was to demonstrate one possible method to build a captcha from the ground up in 1 hour or less.

Please feel free to provide feedback or request clarification, but remember “I am NOT a web designer!”


Captcha (keeping those spammers out of your “business”) Part 02: Extending the PHP the add the challenge code to a database

December 21, 2007

In Part 02 of this series of posts I am going to extend the existing PHP script that generates the captcha image to include functions that add the challenge code to a database, a function to verify a user’s challenge response and some main logic to determine which action to take.

To store the captcha challenge code and the current user’s session id, I have created a simple database “portfolio” and in that database is the “captcha” table (as seen below):

mysql> show columns from captcha;
+-----------+-------------+------+-----+---------+-------+
| Field     | Type        | Null | Key | Default | Extra |
+-----------+-------------+------+-----+---------+-------+
| sid       | varchar(45) | NO   | PRI |         |       |
| challenge | varchar(45) | NO   |     |         |       |
+-----------+-------------+------+-----+---------+-------+
2 rows in set (0.05 sec)

Now let us add a function to add the challenge code to the database:

captcha.php cont…

function update_database($challenge, $sid)
{
  $connection = mysql_connect("localhost", "root", "root");
  $db = mysql_select_db("portfolio");
  $deletesql = "  DELETE
                  FROM    captcha
                  WHERE   sid = '". $sid ."';
                ";
  $updatesql = "  INSERT INTO captcha
                  (sid, challenge)
                  VALUES('". $sid. "','". $challenge ."');
               ";
  mysql_query($deletesql);
  mysql_query($updatesql);
  mysql_close($connection);
}

The new function update_database() opens a connection to a MySQL database, accesses the desired database and runs two queries. The first query deletes all records in the captcha table where the session id is equal to the $sid variable. The second query adds the current $sid of the user and $challenge code from our generateMD5String() function.

Now we need a simple function to test if a user has entered the correct captcha code. This is accomplished by querying the captcha table for a record that contains their sid, and challenge.

captcha.php cont…

function verify_code()
{
  $connection = mysql_connect("localhost", "root", "root");
  $db = mysql_select_db("portfolio");
  $sql = "SELECT * FROM captcha where sid='". $_GET['session_id'] ."' and challenge='". $_GET['challenge'] ."';";
  $result = mysql_query($sql);
  if(mysql_num_rows($result) == 1)
  {
    echo "Authenticated";
  }
  mysql_close($connection);
}

verify_code() uses $_GET values (values from the querystring) of ‘session_id’ and ‘challenge’ to run the query. If the database has a record with the matching session id and challenge, it is safe to assume that the user has passed our automated test.

Before we move to the Javascript and HTML to present a form and handle all the fun ajax, we need some additional logic to drive our captcha.php flow.

captcha.php cont…

switch($_GET['action'])
{
  case "challenge":
    generateCaptcha();
    break;
  case "verify":
    verify_code();
    break;
  default:
    generateCaptcha();
}

With the use of a swtich, we can direct the program flow quite easily. The switch is watching for the ‘action=’ portion of the querystring to determine which function to call.

Putting it all together, we now are able to move to the client side development.

captcha.php complete

<?php

function generateMD5String($chars = 5)
{
  //Generate a random md5 code $chars long
  $md5 = md5(microtime() * mktime());
  $string = substr($md5,0,$chars);
  return $string;
}

function drawRandomLines($im, $recHeight, $recWidth, $lines = 50)
{
  //Add $lines lines to the image with random x, y coordinates
  //within the $recHeight, $recWidth values
  for($i = 1; $i <= $lines; $i++)
  {
    $line = imagecolorallocate($im, 207, 207, 207);
    imageline($im, rand(0, $recHeight), rand(0, $recWidth), rand(0, $recHeight), rand(0, $recWidth), $line);
  }
}

function generateCaptcha($recHeight = 200, $recWidth = 75)
{
  $challenge_string = generateMD5String();    update_database($challenge_string, $_GET['session_id']);

  //Set the header for the PNG
  header("Content-type: image/png");
  $im = imagecreatetruecolor($recHeight, $recWidth);
  //Let's make the font black (0,0,0)
  $font_color = imagecolorallocate($im, 0, 0, 0);
  //For fun I have set the background to a greyish color
  $im_background = imagecolorallocate($im, 229, 229, 229);
  //Fill in the rectangle with color
  imagefilledrectangle($im, 0, 0, $recHeight, $recWidth, $im_background);  

  //Load the gdf to display the challenge word
  $font = imageloadfont("./fonts/alienation48.gdf");

  imagestring($im, $font, rand(0, $recHeight/3), rand(0, $recWidth/3), $challenge_string, $font_color);

  //To add some further complexity, I want to draw lines across the random text
  drawRandomLines($im, $recHeight, $recWidth);

  //Display the image
  imagepng($im);
}   

function update_database($challenge, $sid)
{
  $connection = mysql_connect("localhost", "root", "root");
  $db = mysql_select_db("portfolio");
  $deletesql = "  DELETE
                  FROM    captcha
                  WHERE   sid = '". $sid ."';
                ";
  $updatesql = "  INSERT INTO captcha
                  (sid, challenge)
                  VALUES('". $sid. "','". $challenge ."');
               ";
  mysql_query($deletesql);
  mysql_query($updatesql);
  mysql_close($connection);
}     

function verify_code()
{
  $connection = mysql_connect("localhost", "root", "root");
  $db = mysql_select_db("portfolio");
  $sql = "SELECT * FROM captcha where sid='". $_GET['session_id'] ."' and challenge='". $_GET['challenge'] ."';";
  $result = mysql_query($sql);
  if(mysql_num_rows($result) == 1)
  {
    echo "Authenticated";
  }
  mysql_close($connection);
}      

switch($_GET['action'])
{
  case "challenge":
    generateCaptcha();
    break;
  case "verify":
    verify_code();
    break;
  default:
    generateCaptcha();
}  ?>

Part 03 in this series of posts will focus on using Javascript, Ajax, CSS and PHP to build a form to utilize our captcha.

captcha_003.png


Captcha (keeping those spammers out of your “business”) Part 01: Using PHP to dynamically generate a simple captcha image

December 20, 2007

Yesterday I sat down and remonissed about a simple web site that I put together using Coldfusion. Not too long after the site was published on the internet, the guest book that I had built was beginning to become filled with spam.

Long story short, I realized that I should have built in a captcha (Completely Automated Public Turing Test to tell Computers and Humans Apart) method to make the spammer’s jobs a little more difficult.

You can learn a little more about captcha here.

In this series of posts, I will attempt to develop a simple captcha method that might be used in a guest book, blog or whatever else we can imagine we would want to keep spammers out.

First things first, I want to layout some guidelines for the simple project.

  1. PHP will be used for the server side language (for the sake of my sanity and to cut down on discussions regarding OOP and procedural programming, I will use the latter in this example and append the former in a later follow-up post)
    1. PHP will dynamically generate the captcha image
    2. The captcha challenge code will be saved to something like a session, cookie or database
  2. Javascript will be used as the client side language
    1. Using Ajax, we can request the captcha image and test the user’s response by enhancing the original PHP script
  3. All screen shots will be from Firefox
    1. Don’t get me started on IE
  4. This little project will not attempt to solve every possible spam attack, just make their jobs harder
    1. Read, I am attempting to demonstrate how easy it is to start on the journey of freeing yourself from spam by building a captcha system from scratch

Ok, now that we have the programming languages chosen, it is time to decide how we will code the functions. Since I prefer to keep everything neat, I will break each task into its own function.

Open a text editor, and follow along as I build the server side functions.

captcha.php

function generateMD5String($chars = 5)
{
  //Generate a random md5 code $chars long
  $md5 = md5(microtime() * mktime());
  $string = substr($md5,0,$chars);
  return $string;
}

Simple, we have a nice little function that will generate a random string of letters and or numbers X characters long. Now we can move on to the fun stuff. Next I am going to build a quick function that will use the libgd functions of PHP to build a simple rectangle shaped image, and write the challenge string from our generateMD5String() function overtop the image.

captcha.php cont …

function generateCaptcha($recHeight = 200, $recWidth = 75)
{
  $challenge_string = generateMD5String();  

  //Set the header for the PNG
  header("Content-type: image/png");
  $im = imagecreatetruecolor($recHeight, $recWidth);
  //Let's make the font black (0,0,0)
  $font_color = imagecolorallocate($im, 0, 0, 0);
  //For fun I have set the background to a greyish color
  $im_background = imagecolorallocate($im, 229, 229, 229);
  //Fill in the rectangle with color
  imagefilledrectangle($im, 0, 0, $recHeight, $recWidth, $im_background);   

  //Load the gdf to display the challenge word
  $font = imageloadfont("./fonts/alienation48.gdf"); 

  imagestring($im, $font, rand(0, $recHeight/3), rand(0, $recWidth/3), $challenge_string, $font_color); 

  //Display the image
  imagepng($im);
}

Captcha - Random Positioned Text

The generateCaptcha() function will draw a png with our challenge string from the generateMD5String() and position the text randomly.

You may have noticed that I used the imageloadfont() function to use a font other than the default. A quick search on the internet can provide you with a listing of gdfs that you can use in this script.

So our captcha is looking pretty good so far. To enhance our method, I thought it would be a good idea to draw several lines throughout the png that we are dynamically generating.

captcha.php cont…

function drawRandomLines($im, $recHeight, $recWidth, $lines = 50)
{
  //Add $lines lines to the image with random x, y coordinates
  //within the $recHeight, $recWidth values
  for($i = 1; $i <= $lines; $i++)
  {
    $line = imagecolorallocate($im, 207, 207, 207);
    imageline($im, rand(0, $recHeight), rand(0, $recWidth), rand(0, $recHeight), rand(0, $recWidth), $line);
  }
}

The drawRandomLines() function will draw (by default) 50 lines with random start and end positions on our image. To ensure that the lines overlap the font, call the drawRandomLines() function after you have used the imagestring() function.

Below is the complete PHP code that generates our new captcha image:

captcha.php complete

<?php

function generateMD5String($chars = 5)
{
  //Generate a random md5 code $chars long
  $md5 = md5(microtime() * mktime());
  $string = substr($md5,0,$chars);
  return $string;
}

function drawRandomLines($im, $recHeight, $recWidth, $lines = 50)
{
  //Add $lines lines to the image with random x, y coordinates
  //within the $recHeight, $recWidth values
  for($i = 1; $i <= $lines; $i++)
  {
    $line = imagecolorallocate($im, 207, 207, 207);
    imageline($im, rand(0, $recHeight), rand(0, $recWidth), rand(0, $recHeight), rand(0, $recWidth), $line);
  }
}

function generateCaptcha($recHeight = 200, $recWidth = 75)
{
  $challenge_string = generateMD5String(); 

  //Set the header for the PNG
  header("Content-type: image/png");
  $im = imagecreatetruecolor($recHeight, $recWidth);
  //Let's make the font black (0,0,0)
  $font_color = imagecolorallocate($im, 0, 0, 0);
  //For fun I have set the background to a greyish color
  $im_background = imagecolorallocate($im, 229, 229, 229);
  //Fill in the rectangle with color
  imagefilledrectangle($im, 0, 0, $recHeight, $recWidth, $im_background);  

  //Load the gdf to display the challenge word
  $font = imageloadfont("./fonts/alienation48.gdf");

  imagestring($im, $font, rand(0, $recHeight/3), rand(0, $recWidth/3), $challenge_string, $font_color);

  //To add some further complexity, I want to draw lines across the random text
  drawRandomLines($im, $recHeight, $recWidth);

  //Display the image
  imagepng($im);
}

generateCaptcha();
?>

The resulting image from our new captcha script:

Captcha - Randomly Positioned Lines throughout captcha

In Part 02 of this series of posts, I will enhance the PHP script to support saving the captcha challenge code to the database.