PHP: Anti-spam CAPTCHA using photos

I’m just finishing up a quick PHP project at the moment, which allows anyone to register an account – so as the final step before launching it, I needed to add some form of CAPTCHA system. I tried a couple of 3rd party ones and source code ones and none quite worked for me. This post gives full source for a simple user-friendly photo-based CAPTCHA in PHP. Use at your own risk – but it’s short and easy to integrate.

NB: this was more a quick-and-dirty practice exercise than a serious attempt at a CAPTCHA. I don’t believe in CAPTCHAs, generally – but if you ARE going to use them, it’s best to have a lot of them in the wild, so it’s harder for crackers to do “crack once, spam everywhere”. See the section at the bottom for links to suggestions for other people’s CAPTCHAs that I reckon would be better for production use if you can get them to work :).

Most CAPTCHAs are breakable in under 10 minutes. Personally, I can break nearly every single one I’ve seen (there are a few exceptions, but then you just start hiring bored students etc to mass-crack them for you, by hand). Every now and then I have a go with new ones to prove to myself they’re still easy to break. They generally are.

So, I know none of this is perfect, but I wanted a minimum of:

  1. Implemented in PHP
  2. Highly effective at preventing plain simple automated attacks based on command-line scripts that I could write in under 5 minutes
  3. Doesn’t upset the user / take a long time to process / too hard for humans
  4. Doesn’t look ugly
  5. Simple and quick to integrate with my app
  6. Doesn’t use any external libraries unless those libs are already available as Debian pre-compiled packages

I found a simple idea based on asking humans to rate photos, from a guy called David Spiral, which I rather liked. Nice ideas there. If you never shared this code with anyone, it might work. Unfortunately he’d made a beginner’s mistake and encoded the ID of the “correct” answer in the source code of the HTML page as a “hidden” HTML fom field. Ahem. That took about 5 seconds even for a moronic spammer to crack (HTML source based cracks are easy, and some crack tools / rootkits will even try to automatically (!) discover them for you).

Likewise, he’d implemented a secondary secret algorithm which identified all the images as being incorrect by having an even random ID, or correct by having an odd random ID. That took about 30 seconds to notice and crack without even reading the PHP source (although reading the source confirmed it immediately). Again, I believe there are multiple script kiddie tools that would automatically break this for you, as a spammer. They generally cost something like $20 each to license. A pittance.

After a couple of minutes of thinking and fiddling, I modified his code to replace those two freebies with secrets that are meaningless to the client. The server can still do the one-way check of whether the user submitted the “correct” answer, but at least now the user has to guess it :).

I also made some minor cosmetic changes (e.g. drawing the images in a grid instead of a line, and making the images themselves clickable instead of having to hit the radio buttons).

David has peppered his source with “(c)Copyright” himself etc, which I am not a fan of for such a trivially small example, so I’ve re-implemented it from scratch, and here it is public domain. Do with it what you will.

(but, just to be clear, I do like David’s approach and code, and he did – as he promised – comment it nice and clearly. Thanks, David)

Install / usage instructions

  1. save these 4 PHP files somewhere
  2. edit “secure.php” and change the SALT to a random alphanumeric string of your choice (it doesn’t matter what it is so long as you don’t share it with other people)
  3. create a sub-directory called “folders” and put two images inside, named “cat.jpg” and “dog.jpg”
  4. run index.php to see the captcha in action

To use this in your own application, it ought to be very easy to see how you can “require_once” the index.php file into your own forms (personally I would rename it to something like “captcha-generator.php” once you’ve finished testing it, simply for convenience), and correspondingly insert the process.php (again, rename the file first) inside your form-processing script(s).

Suggestions for improvement:

  1. include a timestamp in the secret value so that the form cannot be captured and replayed infinitely
    • NB: at the moment this is a fatal flaw in the code – replay attacks are *another* built-in feature of the cracking tools
  2. change the image-fetch to instead grab randomized images from images.google.com using hard-coded search-terms
    • this has the benefit that it’s impossible to simply download all the images and save them for comparison/replay attacks – assuming you choose search terms that change frequently
    • I’d prefer to use flickr tagstreams for this, but IIRC that kind of embedding might be against flickr terms of service
    • …and surely someone else has already done this with flickr, but I couldn’t remember what the project was called. I found this one at PlanetJoel, but the site kept timing out when I tried to get it. (this broke one of my cardinal requirements: that the CAPTCHA be quick and easy for human users – but I might go with the planetjoel one anyway when I move my project to production)

PS…

I’m very jetlagged right now. I might well have done something really stupid here that invalidates the whole purpose :). Really … check this for yourself, carefully, and think about what it’s doing and why!

FILE: index.php

<?php
require_once "secure.php";

define('GRIDSIZE', 3);

$numimages = GRIDSIZE * GRIDSIZE;

$index = rand(1, $numimages);
$secret = hashSpecialImageNumber($index);

if( SALT == 'YOUDIDNTCHANGETHIS' )
{
	?>
	<h2>ERROR: Install incomplete</h2>
	<P>
	OK, you did something really stupid: you ignored the one-line install
	instructions, and tried to run this code with the core security part
	disabled
	</P>
	<P>
	To save you from your own ignorance, I'm not going to let the code run
	until you do what you were supposed to, i.e. edit the secure.php file
	and change the value of SALT to some random string of numbers and letters
	</P>
	<P>
	The current value is:
	</P>
	<blockquote>
	<?php echo SALT; ?>
	</blockquote>
	<?php
}
else
{
echo "<form name='captcha' method=\"POST\" action=\"process.php\">";


echo "<table>";
	for ($i = 1; $i <= $numimages; $i++)
	{
		if( $i % GRIDSIZE == 1 )
		{
			echo "<tr>";
		}
		echo "<td align=\"center\">";
				
		$iless = $i - 1;
		echo "<img src=\"image.php?secret=${secret}&n=$i\"";
		echo "onclick=\"document.forms['captcha'].selectedindex[$iless].checked=true\">";
		echo "<br>\n";
		echo "<input type=\"radio\" name=\"selectedindex\" value=\"$i\" \>\n";
		
		echo "</td>";		
		
		if( $i % GRIDSIZE == 0 )
		{
			echo "</tr>";
		}
	}
	
echo "</tr></table>";

echo "<input type=\"hidden\" name=\"secret\" value=\"".$secret."\" \>";

echo "<input type=\"submit\" \>";

echo "</form>";
}?>

FILE: secure.php

<?php
define('SALT', 'YOUDIDNTCHANGETHIS' );
function hashSpecialImageNumber( $num )
{
	return md5( SALT . $num );
}

function checkSpecialImageNumber( $num, $hash )
{
	return md5( SALT . $num ) == $hash;
}
?>

FILE: image.php

<?php
require_once "secure.php";

define('STANDARD_IMAGE','images/dog.jpg');	// modify as required to suit your own images
define('SPECIAL_IMAGE','images/cat.jpg');	// modify as required to suit your own images

header('Content-Type: image/jpg');

if( checkSpecialImageNumber($_GET['n'], $_GET['secret']) )
{
	exit( readfile( SPECIAL_IMAGE ) ); // quit processing after serving the Special image
}
else
{
	exit( readfile( STANDARD_IMAGE ) ); // quit processing after serving the Standard image
}
?>

FILE: process.php

<?php
require_once "secure.php";

// Compare selected image with special image number to see if correct image was selected

$input = $_REQUEST['selectedindex']; // Number of image selected on form

$secret = $_REQUEST['secret']; // Obscured Number of special image from form

if( checkSpecialImageNumber($input, $secret) )
{
	// Your "Passed validation" code here
	
}
else
{
	echo "<H1>FAIL</H1>";
	echo "input number = $input<p>";
	echo "special = $secret<P>";
	echo "hash = ".hashSpecialImageNumber( $input )."<P>";
	echo "checkSpecial = ". checkSpecialImageNumber( $input, $secret )."<P>";
}
?>

2 thoughts on “PHP: Anti-spam CAPTCHA using photos

  1. Robert Basler

    I implemented a ridiculously simple captcha in PHP for http://www.mmorts.com that asks simple mathematical and factual questions. It is totally breakable since there are a finite number of questions, but being a unique captcha for just my site, it isn’t worth the spammers effort. It totally stopped the hundreds of spam posts I was having to wade through each day for a while there. The interesting thing I have noticed is that some of the spambots actually attempt some of the mathematical questions (for example “2 * 5 plus 4” – they answer ten.) So my message here is don’t spend a ton of time on this, pretty much anything should work just fine.

  2. Admin

    By the way, your security certificate is no good in firefox. When I tried to post it said it is for the wrong domain and gave me the opportunity to add an exception.

Leave a Reply

Your email address will not be published. Required fields are marked *