Many moons ago I wrote a blog entry on doing CAPTCHA's in ColdFusion (Quick and dirty CAPTCHA Guide). This guide discussed how to add CAPTCHA images to your forms using third party tools in ColdFusion 7. One of the new features in ColdFusion 8 is built-in CAPTCHA support, so I thought it would be nice to upgrade the guide.
As in the previous version, we will begin with a simple form. This is a contact form with fields for name, email, and comments. Validation is built in for all three fields. The complete, initial form is below:
<cfparam name="form.name" default="">
<cfparam name="form.email" default="">
<cfparam name="form.comments" default="">
<cfset showForm = true>
<cfif structKeyExists(form, "sendcomments")>
<cfset error = "">
<cfif not len(trim(form.name))>
<cfset error = error & "You must include your name, bozo.<br>">
</cfif>
<cfif not len(trim(form.email)) or not isValid("email", form.email)>
<cfset error = error & "Include a valid email address idiot!<br>">
</cfif>
<cfif not len(trim(form.comments))>
<cfset error = error & "It's called a Comment Form, stupid.<br>">
</cfif>
<cfif error is "">
<cfmail to="foo@foo.com" from="#form.email#" subject="Pointless comments from the public" wraptext="75">
From: #form.name# (#form.email#)
Comments:
#form.comments#
</cfmail>
<cfset showForm = false>
</cfif>
</cfif>
<cfif showForm>
<cfif structKeyExists(variables, "error")>
<cfoutput>
<p>
<b>Please correct these errors:<br>
#error#
</b>
</p>
</cfoutput>
</cfif>
<cfoutput>
<form action="#cgi.script_name#" method="post">
<table>
<tr>
<td>Your Name:</td>
<td><input type="text" name="name" value="#form.name#"></td>
</tr>
<tr>
<td>Your Email:</td>
<td><input type="text" name="email" value="#form.email#"></td>
</tr>
<tr>
<td>Your Comments:</td>
<td><textarea name="comments">#form.comments#</textarea></td>
</tr>
<tr>
<td> </td>
<td><input type="submit" name="sendcomments" value="Send Comments"></td>
</tr>
</table>
</form>
</cfoutput>
<cfelse>
<cfoutput>
<p>
Thank you for sending your comments, #form.name#.
</p>
</cfoutput>
</cfif>
Hopefully nothing there was new to you. (But I bet there are still a few of you who have not yet used isValid!). Now that the basic form is done - it won't take long for spammers to begin abusing it. How can we add a CAPTCHA to the form?
ColdFusion 8 makes it incredibly easy with the CFIMAGE tag. One of the many actions it contains is a CAPTCHA action. As you can imagine, this generates a CAPTCHA image. Here is a basic example:
<cfimage action="captcha" width="300" height="75" text="Hello" fonts="verdana,arial">
And this generates:
You have many options to customize the CAPTCHA. Please see the ColdFusion documentation for a complete list, but in general, the attributes you care about are:
- difficulty: This can be low, medium, and high. Low is the default. I find medium to be too strong. You will have to decide for yourself though.
- width/height: I normally use what you see above, 300x75. What's important here though is to ensure your image is big enough to fit the CAPTCHA text. What's cool though is that if you don't use a size big enough for the text, you will get an error telling you exactly how big it does need to be. (Although oddly they tell you one less then the minimum size. So they may say you need width of 100, but what they really mean is 101.)
- fonts: Specifies the fonts for the CAPTCHA. You want to specify this since most servers have a 'ding bats' or other fruity font that will be impossible for folks to decipher.
- text: This is the text of the CAPTHA and will be our focus next.
So in the example above, I used a hard coded text of "hello". While this was easy, it won't take long for spammers to notice that the CAPTCHA text doesn't change. You could always pick a random word from a list. What I do instead, though is use a simple UDF. This UDF will pick random letters and numbers, but will specifically avoid things like "I" (capital 'eye'), "l" (lower case 'el'), and 1. You could modify this UDF to allow for the min and max text strings to be arguments. For me - I thought 4-6 characters was enough. (For an insane CAPTCHA, try the Blingo service. Blingo (that's an affiliate link by the way) is a search engine provider that enters you in a contest for every search you do. When you win (I've won a few times), the CAPTCHA is - I kid you not - something like 20-30 characters. It's the Klingon of CAPTCHAs.)
<cffunction name="makeRandomString" returnType="string" output="false">
<cfset var chars = "23456789ABCDEFGHJKMNPQRS">
<cfset var length = randRange(4,6)>
<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>
Nothing special going on here in the UDF. As you can see it just loops and randomly picks characters. So in order to use this, we could now do this:
<cfimage action="captcha" width="300" height="75" text="#makeRandomString()#" fonts="verdana,arial">
While this works, it leads to our final problem. How do we validate the CAPTCHA text? We could save the text and put it in a hidden form field:
<cfset captcha = makeRandomString()>
<input type="hidden" name="captchatext" vale="#captcha#">
<cfimage action="captcha" width="300" height="75" text="#captcha#" fonts="verdana,arial">
But don't forget that hidden form fields aren't really hidden. It won't take long for spammers to find this either. (Just imagine if these jerks actually spent their time helping the poor?)
So we have a few options here. For this guide (and what I've demonstrated before in presentations) I'm going to use a Hash() of the CAPTCHA text. I could store the value in the Session, but I wanted something that folks could use even if they had session management turned off.
Just take that CAPTCHA string and store a hash of it, like so:
<cfset captcha = makeRandomString()>
<cfset captchaHash = hash(captcha)>
<input type="hidden" name="captchaHash" value="#captchaHash#">
<cfimage action="captcha" width="300" height="75" text="#captcha#" fonts="verdana,arial">
We now have the CAPTHA stored in a hash. On form submission, all we have to do is compare the hash of what you wrote to the hash in the hidden field:
<cfif hash(form.captcha) neq form.captchaHash>
<cfset error = error & "You did not enter the right text. Are you a spammer?<br />">
</cfif>
All in all - not very hard at all. I'm a bit surprised Adobe didn't include a simple 'makeRandomText' function as you really need it for CAPTCHAs. They could also have included it as an attribute to cfimage. But since they added 50 functions and an uber-tag, I can't complain too much. (Ok, I can, but I won't. Today.) Let me know if this guide is helpful. I've included the complete document for our form below. Enjoy.
<cffunction name="makeRandomString" returnType="string" output="false">
<cfset var chars = "23456789ABCDEFGHJKMNPQRS">
<cfset var length = randRange(4,6)>
<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>
<cfparam name="form.name" default="">
<cfparam name="form.email" default="">
<cfparam name="form.comments" default="">
<cfset showForm = true>
<cfif structKeyExists(form, "sendcomments")>
<cfset error = "">
<cfif not len(trim(form.name))>
<cfset error = error & "You must include your name, bozo.<br>">
</cfif>
<cfif not len(trim(form.email)) or not isValid("email", form.email)>
<cfset error = error & "Include a valid email address idiot!<br>">
</cfif>
<cfif not len(trim(form.comments))>
<cfset error = error & "It's called a Comment Form, stupid.<br>">
</cfif>
<cfif hash(form.captcha) neq form.captchaHash>
<cfset error = error & "You did not enter the right text. Are you a spammer?<br />">
</cfif>
<cfif error is "">
<cfmail to="foo@foo.com" from="#form.email#" subject="Pointless comments from the public" wraptext="75">
From: #form.name# (#form.email#)
Comments:
#form.comments#
</cfmail>
<cfset showForm = false>
</cfif>
</cfif>
<cfif showForm>
<cfif structKeyExists(variables, "error")>
<cfoutput>
<p>
<b>Please correct these errors:<br>
#error#
</b>
</p>
</cfoutput>
</cfif>
<cfoutput>
<form action="#cgi.script_name#" method="post">
<table>
<tr>
<td>Your Name:</td>
<td><input type="text" name="name" value="#form.name#"></td>
</tr>
<tr>
<td>Your Email:</td>
<td><input type="text" name="email" value="#form.email#"></td>
</tr>
<tr>
<td>Your Comments:</td>
<td><textarea name="comments">#form.comments#</textarea></td>
</tr>
<tr>
<td>Enter Text Below:</td>
<td><input type="text" name="captcha"></td>
</tr>
<cfset captcha = makeRandomString()>
<cfset captchaHash = hash(captcha)>
<input type="hidden" name="captchaHash" value="#captchaHash#">
<tr>
<td colspan="2"><cfimage action="captcha" width="300" height="75" text="#captcha#" fonts="verdana,arial"></td>
</tr>
<tr>
<td> </td>
<td><input type="submit" name="sendcomments" value="Send Comments"></td>
</tr>
</table>
</form>
</cfoutput>
<cfelse>
<cfoutput>
<p>
Thank you for sending your comments, #form.name#.
</p>
</cfoutput>
</cfif>