A while back I had to implement a captcha on a client’s site. The site owner wanted a simple small captcha (that ruled out reCAPTCHA). We decided to try the new captcha features of ColdFusion8. What you may not realize is that the new captcha feature does not provide the whole captcha system, instead it merely can create captcha images. Its up to you how you implement your captcha system.

Before I get too far just let me state for the record that I dislike captchas and will be happy when they are looked upon like we look at the <blink> tag now. So you don’t need to leave comments telling me how I shouldn’t be using a captcha in the first place. 🙂 The client specifically wanted this feature at this point in time.

I’ve seen some approaches that place the clear text value of the captcha in a hidden field. Then when the form is submitted they compare that value against what the user had typed in. I don’t feel this way is very secure. It will stop simple bots, but you need to guard against more than just that. Sometimes spammers code their bots to work against a specific site. If they find your hidden clear text captcha value, they will easily grab it and use it to submit your form. If this is a simple contact form then you might not have much to worry about, but if its a “send to a friend” feature – watch out, those are high value targets.

Encrypting the hidden value doesn’t help much either. That adds one more step to what the spammer needs to do. They will have to manually read one of your captcha images – then they have the clear text and encrypted values to your captcha system. Now they can just submit that encrypted/plain text pair over and over again to your form.

To really be secure you have to keep track of the captcha value on the server side. I decided to use a database but you could also use an in-memory variable or even use the file system. One of the requirements was to allow the user to ask for a new captcha image without having to reload the whole form. That requires ajax, but cfimage has no built in facilities for easily using it through ajax. But its not too hard to work around. Here is a cfc I created to wrap all this up. My storage and retrieval methods in here use a database but you could easily change them to use the application scope or whatever you like. The important part is to delete the captcha from your system (database, application scope, whatever) after its been solved. This will prevent an attacker from manually decoding one captcha and then using that is his script.

<cfcomponent hint="Creates captchas using cfimage. Supports ajax refresh." output="false">

	<cffunction name="createCaptcha" returntype="struct" hint="Returns a structure containing captcha properties." access="remote">
		<cfargument name="minLength" default="6">
		<cfargument name="maxLength" default="6">
		<cfargument name="id" default="captchaImg" displayname="an ID to be used in the image tag.">
		<cfargument name="difficulty" default="low" hint="low | medium | high">

		<cfset var local = {}>
		<cfset local.retVal = {}>

		<!--- declaring the keys this way ensures they will be delivered to the javascript in lower case, which it expects --->
		<cfset local.retval["imgsrc"] = "">
		<cfset local.retval["captcha_id"] = "">
		<cfset local.retval["imgtag"] = "">

		<cfset local.captchaText = makeCaptchaString(Arguments.minLength,Arguments.maxlength)>

		<!--- store the the captcha value in the DB. Get the ID so it can be used later to look up the captcha value --->
		<cfquery name="local.qryCaptchaID" datasource="#Request.PrimaryDataSource#">
		SET NOCOUNT ON
		INSERT INTO captcha_values (captcha_value) VALUES
		(<cfqueryparam value="#local.captchaText#" cfsqltype="cf_sql_varchar">)
		SELECT SCOPE_IDENTITY() AS captcha_id
		SET NOCOUNT OFF
		</cfquery>

		<!--- need to return the id --->
		<cfset local.retval.captcha_id = local.qryCaptchaID.captcha_id>

		<cfsavecontent variable="local.retVal.imgtag">
			<cfimage action="captcha" text="#local.captchaText#" difficulty="#Arguments.difficulty#">
		</cfsavecontent>

		<!--- tease out the image url, too.  This can be used to refresh the captcha with ajax. --->
		<cfset local.regExResult = ReFind("src=""([^""]*)",local.retVal.imgtag,1,1)>
		<cfset local.retVal.imgsrc = Mid(local.retVal.imgtag,local.regExResult.pos[2],local.regExResult.len[2])>

		<!--- need to add an ID so we can manipulate with JavaScript. --->
		<cfset local.retVal.imgtag = Replace(local.retVal.imgtag,"<img","<img id=""#Arguments.id#""","one")>

		<cfreturn local.retVal>

	</cffunction>

	<cffunction name="getCaptchaText" returntype="string" hint="Returns the text associated with a captcha_id" access="public">
		<cfargument name="captcha_id">

		<cfset local = {}>

		<cfquery name="local.qryCaptcha" datasource="#Request.PrimaryDataSource#">
		SELECT captcha_value FROM captcha_values WHERE
		captcha_id = <cfqueryparam value="#Arguments.captcha_id#" cfsqltype="cf_sql_integer">
		</cfquery>

		<cfif local.qryCaptcha.RecordCount EQ 0>
			<cfreturn "">
		</cfif>

		<!--- else we did find a captcha with this ID.  Now delete it, and return the value we found --->
		<cfquery datasource="#Request.PrimaryDataSource#">
		DELETE FROM captcha_values WHERE
		captcha_id = <cfqueryparam value="#Arguments.captcha_id#" cfsqltype="cf_sql_integer">
		</cfquery>

		<cfreturn local.qryCaptcha.captcha_value>
	</cffunction>

	<cffunction name="makeCaptchaString" returnType="string" output="false" access="private">
		<cfargument name="minLength">
		<cfargument name="maxLength">

		<!--- Don't use any characters that can be confused with each other --->
		<cfset var chars = "23456789ABCDEFGHJKMNPQRS">
		<cfset var length = randRange(Arguments.minLength,Arguments.maxLength)>
		<cfset var result = "">
		<cfset var i = "">
		<cfset var char = "">

		<cfscript>
		for(i=1; i <= length; i++) {
			char = mid(chars, randRange(1, len(chars)),1);
			result&=char;
		}
		</cfscript>

		<cfreturn result>
	</cffunction>
</cfcomponent>

Then in your form page you need a little javascript to allow the user to refresh the image.

<cfajaxproxy cfc="cfimageCaptcha" jsclassname="cfimageCaptcha">
<script language="JavaScript">
function refreshCaptcha() {
	var jsCaptcha = new cfimageCaptcha();
	jsCaptcha.setCallbackHandler(populateCaptcha);
	jsCaptcha.createCaptcha();
}

function populateCaptcha(result) {
	var imgTag = document.getElementById("captchaImg");
	var hiddenTag = document.getElementById("captcha_id");

	imgTag.src = result.imgsrc;
	hiddenTag.value = result.captcha_id;

}
</script>

Then display the actual captcha on the page:

<cfset myCaptcha = CreateObject("component","cfimageCaptcha").createCaptcha()>
#myCaptcha.imgtag#<a href="JavaScript:refreshCaptcha()">change code</a><br />
Type in the letters you see above: <input type="text" name="captcha_value">
<!--- need to pass along the ID of the captcha so we can look it up later --->
<input type="hidden" name="captcha_id" id="captcha_id" value="#myCaptcha.captcha_id#">

With me so far? Now here is how you validate the captcha on your “action” page:

<cfif Form.captcha_value NEQ CreateObject("component","cfimageCaptcha").getCaptchaText(Form.captcha_id)>
       <p>Throw your failed captcha error here!</p>
</cfif>

Here is the SQL I used for my captcha table.

CREATE TABLE [captcha_values] (
	[captcha_id] [int] IDENTITY (1, 1) NOT NULL ,
	[captcha_value] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
	[date_added] [smalldatetime] NULL CONSTRAINT [DF_captcha_values_date_added] DEFAULT (getdate())
) ON [PRIMARY]
GO

And here is the query I run nightly to clear out abandoned entries that are older than 24 hours:

DELETE FROM captcha_values WHERE
DateDiff(hour,date_added,CURRENT_TIMESTAMP) > 24

3 Comments

  1. Misty says:

    Too Good Mate, I was Just going to try this method but i found your post, Cool Enough my clients also demand this Thanks

  2. Marc says:

    Thanks for this code, exactly what I was looking for. I've expanded it quite a bit to be able to handle storing the captcha information in either the application or session scopes as well so that DB isn't needed.

  3. Toby Scheidegger says:

    Thanks for your post almost dental implants overseas I have been looking at overseas dental services.Dental crowns overseas also seem to give great savings the results also seem to be excellent.Cheap dental work overseas seems to gathering popularity.