From 8b23a8163273bcf9bb87d402d0691c81cc1bb7d8 Mon Sep 17 00:00:00 2001
From: Damien Regad <dregad@mantisbt.org>
Date: Fri, 2 Jan 2015 01:39:14 +0100
Subject: [PATCH] Cache generated captcha to ensure uniqueness

This is an improvement over the earlier fix which seeded the random
number generator with the captcha's key.

As Florent pointed out, the cure was worse than the disease as it
reduced the effective number of distinct captchas to a mere 2^31, making
it easy for an attacker to precompute the lot to bypass the challenge.

In addition, debug mode now works in context of make_captcha_img.php,
i.e. it doesn't set the image/jpeg content type header, and displays the
generated captcha as an image within an html page.

Fixes #17984
---
 core/constant_inc.php |  1 +
 make_captcha_img.php  | 40 +++++++++++++++++++++++++++++++++++-----
 signup.php            |  3 +++
 signup_page.php       |  1 +
 4 files changed, 40 insertions(+), 5 deletions(-)

diff --git a/core/constant_inc.php b/core/constant_inc.php
index 25715ca..7e04801 100644
--- a/core/constant_inc.php
+++ b/core/constant_inc.php
@@ -542,3 +542,4 @@ define( 'PASSWORD_MAX_SIZE_BEFORE_HASH', 1024 );
 define( 'SECONDS_PER_DAY', 86400 );
 
 define( 'CAPTCHA_KEY', 'captcha_key' );
+define( 'CAPTCHA_IMG', 'captcha_image' );
diff --git a/make_captcha_img.php b/make_captcha_img.php
index 28391d3..cda9b29 100644
--- a/make_captcha_img.php
+++ b/make_captcha_img.php
@@ -147,10 +147,8 @@
 				if($this->debug) echo "\n<br />-Captcha-Debug: Set image dimension to: (".$this->lx." x ".$this->ly.")";
 			}
 
-			function make_captcha( $private_key )
+			function generate_captcha( $private_key )
 			{
-				srand( session_get( CAPTCHA_KEY ) );
-
 				if($this->debug) echo "\n<br />-Captcha-Debug: Generate private key: ($private_key)";
 
 				// create Image and set the apropriate function depending on GD-Version & websafecolor-value
@@ -218,7 +216,7 @@
 				}
 
 				// generate Text
-				if($this->debug) echo "\n<br />-Captcha-Debug: Fill forground with chars and shadows: (".$this->chars.")";
+				if($this->debug) echo "\n<br />-Captcha-Debug: Fill foreground with chars and shadows: (".$this->chars.")";
 				for($i=0, $x = intval(rand($this->minsize,$this->maxsize)); $i < $this->chars; $i++)
 				{
 					$text	= utf8_strtoupper(substr($private_key, $i, 1));
@@ -239,10 +237,42 @@
 					}
 					$x += (int)($size + ($this->minsize / 5));
 				}
-				header('Content-type: image/jpeg');
+
+				# Generate the JPEG
+				ob_start();
 				@ImageJPEG($image, null, $this->jpegquality);
+				$jpg = ob_get_contents();
+				ob_end_clean();
+
 				@ImageDestroy($image);
 				if($this->debug) echo "\n<br />-Captcha-Debug: Destroy Imagestream.";
+
+				return $jpg;
+			}
+
+			function make_captcha( $private_key )
+			{
+				# Retrieve previously image generated from session cache
+				$t_image = session_get( CAPTCHA_IMG, null );
+
+				if( is_null( $t_image ) ) {
+					$t_image = $this->generate_captcha( $private_key );
+					if( $this->debug ) {
+						echo "\n<br />-Captcha-Debug: Caching generated image.";
+					}
+					session_set( CAPTCHA_IMG, $t_image );
+				} elseif( $this->debug ) {
+					echo "\n<br />-Captcha-Debug: Retrieved image from cache.";
+				}
+
+				# Output
+				if( $this->debug ) {
+					echo "\n<br />-Captcha-Debug: Generated image (" . strlen( $t_image ) . " bytes): "
+						. '<img src="data:image/jpeg;base64,' . base64_encode( $t_image ) . '">';
+				} else {
+					header('Content-type: image/jpeg');
+					echo $t_image;
+				}
 			}
 
 			/** @private **/
diff --git a/signup.php b/signup.php
index b63e772..8ee2449 100644
--- a/signup.php
+++ b/signup.php
@@ -63,6 +63,9 @@
 		if ( $t_key != $f_captcha ) {
 			trigger_error( ERROR_SIGNUP_NOT_MATCHING_CAPTCHA, ERROR );
 		}
+
+		# Clear captcha cache
+		session_delete( CAPTCHA_IMG );
 	}
 
 	email_ensure_not_disposable( $f_email );
diff --git a/signup_page.php b/signup_page.php
index 3a8c725..b7a302c 100644
--- a/signup_page.php
+++ b/signup_page.php
@@ -67,6 +67,7 @@
 	$t_allow_passwd = helper_call_custom_function( 'auth_can_change_password', array() );
 	if( ON == config_get( 'signup_use_captcha' ) && get_gd_version() > 0 && ( true == $t_allow_passwd ) ) {
 		session_set( CAPTCHA_KEY, mt_rand() );
+		session_delete( CAPTCHA_IMG );
 
 		# captcha image requires GD library and related option to ON
 ?>
-- 
1.9.1

