PHP: Creating a CAPTCHA with no Cookies
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.chirpinternet.eu
// Please acknowledge use of this code by including this header.
class CaptchaNoCookie
{
protected static $encryption_key;
protected $font = "didot/GFSDidotBold.otf";
protected $fontsize = 28;
protected $code = "";
protected $crypted = "";
public $digits = 6;
public function __construct()
{
self::$encryption_key = gethostname() . __CLASS__;
// generate CAPTCHA code
for($i=0; $i < $this->digits; $i++) {
$this->code .= rand(0, 9);
}
}
public function crypted()
{
if(!$this->crypted) {
$cryptor = new Cryptor(self::$encryption_key);
$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(self::$encryption_key);
$decrypted_token = $cryptor->decrypt($crypted);
return $user_input == $decrypted_token;
}
}
?>
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:
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.chirpinternet.eu
// 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.
<?PHP
$myCaptcha = new CaptchaNonceNoCookie();
?>
<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>
Remember to also use CaptchaNonceNoCookie::validate in your form handler for validation, or you'll simply be collecting tempfiles for no reason.
<?PHP
if(!CaptchaNonceNoCookie::validate($_POST['crypted'], $_POST['captcha'])) {
die("Sorry, the CAPTCHA code you entered was not correct!");
}
// CAPTCHA passed validation
?>
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.
We use a CRON script to clear out unused temporary files after 30 minutes:
#!/bin/bash
/usr/bin/find /path/to/captcha/files -type f -mmin +30 -delete
Related Articles - Form Validation
- HTML HTML5 Form Validation Examples
- HTML Validating a checkbox with HTML5
- JavaScript Preventing Double Form Submission
- JavaScript Form Validation
- JavaScript Date and Time
- JavaScript Password Validation using regular expressions and HTML5
- JavaScript A simple modal feedback form with no plugins
- JavaScript Allowing the user to toggle password INPUT visibility
- JavaScript Tweaking the HTML5 Color Input
- JavaScript Credit Card numbers
- JavaScript Counting words in a text area
- PHP Basic Form Handling in PHP
- PHP Protecting forms using a CAPTCHA
- PHP Creating a CAPTCHA with no Cookies
- PHP Measuring password strength
Chris 2 August, 2020
Nice script but how can we add a refresh button ?
Like in the chpater 7 from www.the-art-of-web.com/php/captcha/
In the other examples, the CAPTCHA loads as an image file which sets a session/cookie for validation when the form is submitted. Reloading the image also loads a new cookie value.
But here both the CAPTCHA and the validation string are stored in the page using HTML/CSS. You would need an Ajax script to refresh both the hidden form field and the CAPTCHA in the HTML.
AA-T 11 October, 2018
Nice and very useful article, thanks.
Would it be possible to use $_SESSION instead of a temporary file? If so I might give it a try.
The entire point of this article is to create a CAPTCHA without using cookies. This earlier article shows you how to do it with $_SESSION (which is actually a form of temporary file anyway).