skip navigation

PHP: Protecting forms using a CAPTCHA

The CAPTCHA approach to securing forms is not new - it first appeared in the late 90's for domain name submissions to search engines and the like - but with the exponential growth of scripted exploits it's coming to the fore once again. The main targets are Guestbook and Contact forms, but any website with a form could be vulnerable to misuse.

The code presented here shows you how to create a simple CAPTCHA graphic with random lines and digits and how to incorporate it into an HTML form to prevent automated submission by malicious scripts.

Creating a CAPTCHA graphic using PHP

The following code needs to be saved as a stand-along PHP file (we call it captcha.php). This file creates a PNG image containing a series of five digits. It also stores these digits in a session variable so that other scripts can know what the correct code is and validate that it's been entered correctly.

<?PHP // Adapted for The Art of Web: www.the-art-of-web.com // Based on PHP code from: php.webmaster-kit.com // Please acknowledge use of this code by including this header. // initialise image with dimensions of 120 x 30 pixels $image = @imagecreatetruecolor(120, 30) or die("Cannot Initialize new GD image stream"); // set background to white and allocate drawing colours $background = imagecolorallocate($image, 0xFF, 0xFF, 0xFF); imagefill($image, 0, 0, $background); $linecolor = imagecolorallocate($image, 0xCC, 0xCC, 0xCC); $textcolor = imagecolorallocate($image, 0x33, 0x33, 0x33); // draw random lines on canvas for($i=0; $i < 6; $i++) { imagesetthickness($image, rand(1,3)); imageline($image, 0, rand(0,30), 120, rand(0,30), $linecolor); } session_start(); // add random digits to canvas $digit = ''; for($x = 15; $x <= 95; $x += 20) { $digit .= ($num = rand(0, 9)); imagechar($image, rand(3, 5), $x, rand(2, 14), $num, $textcolor); } // record digits in session variable $_SESSION['digit'] = $digit; // display image and clean up header('Content-type: image/png'); imagepng($image); imagedestroy($image); ?>

The output of this script appears as follows (reload to see it change):

CAPTCHA

This image is meant to be difficult for 'robots' to read, but simple for humans (the Turing test). You can make it more difficult for them by addition of colours or textures, or by using different fonts and a bit of rotation.

We've simplified the script presented above as much as possible so that you can easily customise it for your site and add more complexity as necessary.

Adding a CAPTCHA to your form

In your HTML form you need to make sure that the CAPTCHA image is displayed and that there's an input field for people to enter the CAPTCHA code for validation. Here's a 'skeleton' of how the HTML code for your form might appear:

<form method="POST" action="form-handler" onsubmit="return checkForm(this);"> ... <p><img src="/captcha.php" width="120" height="30" border="1" alt="CAPTCHA"></p> <p><input type="text" size="6" maxlength="5" name="captcha" value=""><br> <small>copy the digits from the image into this box</small></p> ... </form>

If you're using JavaScript form validation then you can test that a code has been entered in the CAPTCHA input box before the form is submitted. This will confirm that exactly five digits have been entered, but not say anything about whether they're the right digits as that information is only available on the server-side ($_SESSION) data.

So again, here's a skeleton of how your JavaScript form validation script might appear:

function checkForm(form) { ... if(!form.captcha.value.match(/^\d{5}$/)) { alert('Please enter the CAPTCHA digits in the box provided'); form.captcha.focus(); return false; } ... return true; }

Finally, in the server-side script that is the target of the form, you need to check that the code entered in the form by the user matches the session variable set by the captcha.php script:

if($_POST && all required variables are present) { ... session_start(); if($_POST['captcha'] != $_SESSION['digit']) die("Sorry, the CAPTCHA code entered was incorrect!"); session_destroy(); ... }

Note: It's important to call session_start both in the captcha.php script (when seting the session variable) and in the server-side validation script (in order to retrieve the value) as those files are processed separately and can't otherwise share information.

You can see this code working in our Feedback form which appears as a link on every page.

Putting it all together

There has been feedback sent by a number of people confused about which code to put where to get this working on their own website. To try and make it clearer I've put together a couple of diagrams which illustrate the two most common solutions.

Here you can see illustrated the simplest and most common setup, but by no means the best solution. The form is checked using JavaScript and then POSTed to another page/script where the data is processed:

A more 'professional' solution involves a practice called Post/Redirect/Get (PRG) which means that the data is first processed and then the user is redirected to a landing page:

This avoids a number of issues including problems caused when someone reloads the landing page which in the first configuration would cause all the POST data to be re-submitted.

This can also be implemented using three scripts where the form handler has it's own file and decides whether to redirect back to the FORM or forward to the landing page depending on whether the data validates.

In any case the PHP form handler code needs to appear as the first item before any HTML code is generated.

Upgrading the CAPTCHA to block new bots

The CAPTCHA image presented above was 'cracked' after a matter of months by one or two bots. Fortunately a few small changes to the code can send them packing at least for a while.

Here's some code to 'jazz up' our CAPTCHA to give it a better chance of being bot-proof. The sections of code that have been changed are highlighted:

<?PHP // Adapted for The Art of Web: www.the-art-of-web.com // Based on PHP code from: php.webmaster-kit.com // Please acknowledge use of this code by including this header. // initialise image with dimensions of 120 x 30 pixels $image = @imagecreatetruecolor(120, 30) or die("Cannot Initialize new GD image stream"); // set background and allocate drawing colours $background = imagecolorallocate($image, 0x66, 0x99, 0x66); imagefill($image, 0, 0, $background); $linecolor = imagecolorallocate($image, 0x99, 0xCC, 0x99); $textcolor1 = imagecolorallocate($image, 0x00, 0x00, 0x00); $textcolor2 = imagecolorallocate($image, 0xFF, 0xFF, 0xFF); // draw random lines on canvas for($i=0; $i < 6; $i++) { imagesetthickness($image, rand(1,3)); imageline($image, 0, rand(0,30), 120, rand(0,30) , $linecolor); } session_start(); // add random digits to canvas using random black/white colour $digit = ''; for($x = 15; $x <= 95; $x += 20) { $textcolor = (rand() % 2) ? $textcolor1 : $textcolor2; $digit .= ($num = rand(0, 9)); imagechar($image, rand(3, 5), $x, rand(2, 14), $num, $textcolor); } // record digits in session variable $_SESSION['digit'] = $digit; // display image and clean up header('Content-type: image/png'); imagepng($image); imagedestroy($image); ?>

And here is the modified CAPTCHA graphic produced by the new code:

CAPTCHA

All we've done here is changed the background colour from white to green, the lines from grey to light green and the font colour from black to a mixture of white and black.

This method has now also been cracked by a small number of bots. In recent days we've seen 10-20 succesful exploits a day, but we're not going to give up. A new version of the CAPTCHA graphic is shown in the next section below.

For information on how the CAPTCHA images can be cracked, read this article.

Yet another CAPTCHA

So here's the latest version that we're using on live websites. The main change from those presented above is that we now use a larger range of fonts to confuse the spambots. You can find a good resource for GDF fonts under References below. The positioning of the lines has also changed to make it more random.

CAPTCHA

Once this one is cracked the options become more complex. We'll probably have to start rotating some of the digits or applying other kinds of distortion. Or Microsoft could invent a secure operating system and put an end to botnets once and for all!

Usability improvements

It's the little things that make your visitors more relaxed about filling in forms. The code below has been modified to limit the input to only numbers using the onkeyup event, and adding an option to reload/refresh the CAPTCHA image in the case that it's not readable. .... <p><img id="captcha" src="/captcha.php" width="120" height="30" border="1" alt="CAPTCHA"> <small><a href="#" onclick=" document.getElementById('captcha').src = '/captcha.php?' + Math.random(); document.getElementById('captcha_code').value = ''; return false; ">refresh</a></small></p> <p><input id="captcha_code" type="text" name="captcha" size="6" maxlength="5" onkeyup="this.value = this.value.replace(/[^\d]+/g, '');"> <small>copy the digits from the image into this box</small></p> ...

In your form, the CAPTCHA section will then appear something like the following:

Security Check CAPTCHA refresh <- copy the digits from the image into this box

You can see this code in action on our Feedback page which is linked at the bottom of each page.

References

< PHP


User Comments and Notes

Lisa 3 February, 2007

First of all, your captcha instructions are the best I've seen yet! I almost got it going but I'm confused about the last part where I need to add the session start variable.

My PHP feedback forms has a .php validation file that validates everything on the form before the form is submitted. Is this the target file you mentioned? I'm not sure where to put that session start info into my script. I pasted it into that file but I get the captcha code is incorrect no matter what I enter. If you could help that would be great thanks!

By definition PHP can validate only AFTER the form is submitted. In your PHP code you need to call session_start() before any $_SESSION variables become available, and then destroy the session. If you have problems try using the print_r function to display the $_POST and $_SESSION data to make sure they are populated. If your form uses GET instead of POST as the submission method then replace $_POST with $_GET.

Paul Smith 9 March, 2008

1st of all thank you for a straightforward script that works well.

You mention adding colour to the background and making the numbers black & white (as in your own Captcha). Can you provide a sample script with these changes.

You can find the code for the second CAPTCHA now on the page. We only make public new versions of the code as and when we upgrade our own 'live' version, which has now happened.

Brian 14 November, 2008

Thanks for a very informative and easy to follow tutorial!

Mewp 3 August, 2009

I have only one question: how is it better than ReCAPTCHA [recaptcha.net]?

also, your CAPTCHA is barely readable sometimes (due to small numbers), and have few weaknesses:
By scanning the first row I can determine color of background and lines. If i do that, i know that any other color is a number, and I have only to detect characters. Assuming three fonts in three sizes, there are 3*3*10 possibilities of generated character bitmap. That's rather easy to break for a specialized bot.

Your idea to rotate numbers wouldn't work either (I've seen bots break that).

You only have one question?

  • Noone said the above code was 'better' than any other system. This is a tutorial showing how to start writing your own;
  • Any CAPTCHA can be broken by a 'specialized' bot. We're only looking at protecting from 'wild' bots, which it does quite well;
  • I will report if/when the last CAPTCHA in the article is broken and update the code. It's worked so far since early 2008

Gary 14 October, 2009

HI, I am new to this, maybe you can help.
Where exactly is this suposed to go?

if($_POST && all required variables are present) {
...
}

I've tried to illustrate this in the section "Putting it all together". Look for where "PHP form handler" appears in the graphics.

Send Feedback

Send Your Feedback (will not be published) (optional) CAPTCHA refresh <- copy the digits from the image into this box

[top]