skip to content

PHP: Creating a CAPTCHA with no Cookies

 Tweet Share0 Tweets

As an exercise we're combining our Captcha class which normally requires a SESSION variable, with our new Cryptor class, to implement a CAPTCHA test that doesn't rely on browser cookies.

Be warned, the CaptchaNoCookie class is not for public deployment as-is, as once a valid code/encrypted code pair have been determined, the form can be used repeatedly. Instead use the extended CaptchaNonceNoCookie class.

The CaptchaNoCookie class

In our recent article on two-way encryption in PHP we developed the Cryptor class for encrypting and decrypting strings. Previously, we've also presented code for creating a CAPTCHA, though not (yet) in it's own class.

So what we have here is a stripped down version of our CAPTCHA class where instead of using a SESSION to store the CAPTCHA code for verification, we instead include the code encrypted in a hidden form field.

The new CaptchaNoCookie class is defined as follows:

<?PHP // Original PHP code by Chirp Internet: www.chirp.com.au // Please acknowledge use of this code by including this header. class CaptchaNoCookie { protected $font = "didot/GFSDidotBold.otf"; protected $fontsize = 28; protected $code = ""; protected $crypted = ""; public $digits = 6; public function __construct() { // generate CAPTCHA code for($i=0; $i < $this->digits; $i++) { $this->code .= rand(0, 9); } } public function crypted() { if(!$this->crypted) { $cryptor = new Cryptor(gethostname() . __CLASS__); $this->crypted = $cryptor->encrypt($this->code); } return $this->crypted; } public function display() { // calculate required canvas size $box = imagettfbbox($this->fontsize, 0, $this->font, "88888"); $boxwidth = abs(round($box[4] - $box[0]) * 1.2); $boxheight = abs(round($box[5] - $box[1])); $width = round($boxwidth * 1.2); $height = round($boxheight * 1.4); // create image canvas $image = @imagecreatetruecolor($width, $height) or die("Cannot Initialize new GD image stream"); // background fill $background = imagecolorallocate($image, 0x66, 0xCC, 0xFF); imagefill($image, 0, 0, $background); // allocate line colours $linecolor = imagecolorallocate($image, 0x33, 0x99, 0xCC); $textcolor1 = imagecolorallocate($image, 0x00, 0x00, 0x00); $textcolor2 = imagecolorallocate($image, 0xFF, 0xFF, 0xFF); // draw random ilnes for($i=0; $i < 8; $i++) { imagesetthickness($image, rand(1, 3)); imageline($image, rand(0, $width), 0, rand(0, $width), $height, $linecolor); } // paint digits on canvas for($i=0; $i < $this->digits; $i++) { $x = ceil($i * $boxwidth/$this->digits); $angle = rand(-20, 20); $color = (rand() % 2) ? $textcolor1 : $textcolor2; $xpos = round($width/10 + $x); $shim = ($height - $boxheight)/2; // don't ask $ypos = rand($boxheight - $shim, $boxheight + $shim); imagettftext($image, $this->fontsize, $angle, $xpos, $ypos, $color, $this->font, $this->code{$i}); } // return image as Data URI ob_start(); imagepng($image); $image_data = "data:image/png;base64," . base64_encode(ob_get_clean()); imagedestroy($image); return $image_data; } public static function validate($crypted, $user_input) { $cryptor = new Cryptor(gethostname() . __CLASS__); $decrypted_token = $cryptor->decrypt($crypted); return $user_input == $decrypted_token; } } ?>

expand code box

The main change to our earlier CAPTCHA code is that we're displaying the CAPTCHA as an inline Data URI (base64 encoded string) rather than generating an actual image file. That saves us creating an extra script.

The font path is defined relative to the library-defined font path:
e.g. /usr/share/fonts/truetype

Displaying the CAPTCHA in a form

Displaying the CAPTCHA in your form is a matter of invoking the CaptchaNoCookie to generate both the PNG image and the encrypted digits:

<?PHP $myCaptcha = new CaptchaNoCookie(); ?> <form method="POST" action="..."> <input type="hidden" name="crypted" value="<?= $myCaptcha->crypted(); ?>"> <p><img src="<?= $myCaptcha->display(); ?>" alt=""></p> <p>CAPTCHA: <input type="text" required pattern="\d{<?= $myCaptcha->digits; ?>}" name="captcha"><br> <input type="submit"></p> </form>

The output, with a bit of formatting, and the hidden field made visible, will look something like this:

CAPTCHA Authentication " alt=""> <- copy the digits from the image

We're using a touch of HTML5 Form Validation to control user input, and would normally also validate all POST variables in the PHP form handler.

A drawback of the Data URI approach is that we can no longer 'refresh' the image to present a new code without reloading the whole form, or using Ajax to fetch new values.

Validating user input

When the form is submitted both the digits entered by the user and the encrypted string will be received. We then validate the CAPTCHA by decrypting the encrypted string and comparing it to the user input:

<?PHP if(!CaptchaNoCookie::validate($_POST['crypted'], $_POST['captcha'])) { die("Sorry, the CAPTCHA code you entered was not correct!"); } // CAPTCHA passed validation ?>

Note that because validate() is a static method of the CaptchaNoCookie class we can call it directly without instantiating a new object.

Why it isn't (yet) secure

The above approach seems promising. We're using a high grade encryption to transmit the required digits, and it will be uniquely generated each time by our Captor class, so what's wrong?

The problem is that by simply verifying that the user input matches the encrypted value we're not preventing the same values from being re-submitted over and over again, and it's only a matter of time before some spambot works that out.

Some possible solutions:

Using temporary files

One fix would be to modify the CaptchaNoCookie class so that every time a code is generated, it creates a temporary file on the server. And deletes said file after the code has been successfully used.

touch(/tmp/$crypted)
file_exists(/tmp/$crypted)
unlink(/tmp/$crypted)

That way if the same code is resubmitted, there will be no associated file and verification can be aborted. Unused code files can be garbage-collected.

Embedding a timestamp

We can encode extra information into the encrypted string, such as the timestamp, user ip address, etc, and use that to determine whether the submitted values should be validated.

The CaptchaNonceNoCookie Class

As indicated above, the CaptchaNoCookie class is not usable as-is because it allows the same code to be reused over and over. To that end we've extended the class to add a system for creating and checking a temporary file for each generated CAPTCHA.

<?PHP // Original PHP code by Chirp Internet: www.chirp.com.au // Please acknowledge use of this code by including this header. class CaptchaNonceNoCookie extends CaptchaNoCookie { private static function tempfile($crypted) { return sys_get_temp_dir() . DIRECTORY_SEPARATOR . str_replace(DIRECTORY_SEPARATOR, "_", $crypted); } public function display() { touch(self::tempfile($this->crypted)); return parent::display(); } public static function validate($crypted, $user_input) { if(file_exists(self::tempfile($crypted))) { if(parent::validate($crypted, $user_input)) { unlink(self::tempfile($crypted)); return true; } else { // validation failed } } else { // code already used or expired } return false; } } ?>

The new CaptchaNonceNoCookie class extends the old class and can be called in exactly the same fashion. The only difference being that a temporary file is created when the CAPTCHA is displayed, and deleted after successful validation.

Remember to also use CaptchaNonceNoCookie::validate for validation, or you'll simply be collecting tempfiles for no reason.

We now have a fully functioning secure CAPTCHA system with single-use codes. Feel free to try it out, and if you have any comments or questions you can get in touch using the Feedback button below.

< PHP

Send a message to The Art of Web:


used only for us to reply, and to display your gravatar.

<- copy the digits from the image into this box

press <Esc> or click outside this box to close

Post your comment or question
top