Quick and Dirty ColdFusion 8 CAPTCHA Guide

This post is more than 2 years old.

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 = "">
&lt;cfscript&gt;
for(i=1; i &lt;= length; i++) {
	char = mid(chars, randRange(1, len(chars)),1);
	result&=char;
}
&lt;/cfscript&gt;
	
&lt;cfreturn result&gt;

</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 = "">
&lt;cfscript&gt;
for(i=1; i &lt;= length; i++) {
	char = mid(chars, randRange(1, len(chars)),1);
	result&=char;
}
&lt;/cfscript&gt;
	
&lt;cfreturn result&gt;

</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>

  &lt;cfset captcha = makeRandomString()&gt;
  &lt;cfset captchaHash = hash(captcha)&gt;
  &lt;input type="hidden" name="captchaHash" value="#captchaHash#"&gt;
  
  &lt;tr&gt;
  	 &lt;td colspan="2"&gt;&lt;cfimage action="captcha" width="300" height="75" text="#captcha#"  fonts="verdana,arial"&gt;&lt;/td&gt;
  &lt;/tr&gt;
  
  &lt;tr&gt;
     &lt;td&gt;&nbsp;&lt;/td&gt;
     &lt;td&gt;&lt;input type="submit" name="sendcomments" value="Send Comments"&gt;&lt;/td&gt;
  &lt;/tr&gt;

</table> </form> </cfoutput> <cfelse> <cfoutput> <p> Thank you for sending your comments, #form.name#. </p> </cfoutput> </cfif>

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by James Moberg posted on 3/30/2008 at 11:13 PM

Your captcha form is still extremely spammable... I made this same mistake 2 years ago.

Spammers will manually build a profile based on your form. They will store the captchaHash and enter in the correct value in the "captcha" field. Since captchaHash will always equal the hashed captcha value, they can store the 2 parameters and then post as many bogus emails, names and comments as they desire while using a fake http_referer and user_agent from multiple compromised IP addresses.

In order to fix this, add a changing unpublished, non-displayable prefix/suffix to the generated hash to ensure that it can't be saved and used forever:
<cfset captchaHash = hash("#DateFormat(Now(),'YYYYMMDD')##captcha#")>

Comment 2 by Raymond Camden posted on 3/30/2008 at 11:24 PM

Wow, thats a pretty easy way around it.

So for your solution, I take the user's value, add the prefix, then hash it.

How do you handle edge cases - like me hitting your form at 11:59PM? For dates you could get around this by checking for both today and yesterday though.

Comment 3 by James Moberg posted on 3/30/2008 at 11:41 PM

Yes, if part of the hashed value is the date, you can easily attempt to convert it back to a date and validate [isDate()] and then perform a DateDiff() function. 24 hours or even 48 hours is not too long for a dynamic parameter to be valid... you just don't want values to be archived and eternally exploitable.

Comment 4 by James Moberg posted on 3/30/2008 at 11:46 PM

Whoops... I forgot. (I posted too quickly.) "Hash" is one way. You'll have to "encode" that values so that you can "decode" after posting.

Comment 5 by Raymond Camden posted on 3/31/2008 at 1:44 AM

Well, what I was thinking was

if(today + userentry -> hashed) neq the hash

then try (yesterday + userentry -> hashed)

You could even try yesterday conditionally - ie, only in the first hour after midnight.

Maybe another option would be to use unique hashes. Ie, as soon as you use it, don't use it again. You could store this in the app scope. It wouldn't persist forever, but would stop immediate attacks.

Comment 6 by James Moberg posted on 3/31/2008 at 2:01 AM

If you encoded the values, you could also pass the key ID in it... so that all values are passed in a single encrypted token. Kinda like:

Hash = CFusion_Encrypt("ID-YYYYMMDD-CaptchaWord", KEY)

HashDecoded = CFusion_Decrypt(Hash, KEY)
ID = ListFirst(HashDecoded,"-")
ValidDate = ListGetAt(HashDecoded,2,"-")
CaptchaWord = ListLast(HashDecoded,"-"))

... and then perform any needed validation. Of course, you'd have to substitute CFusion_Encrypt with something else to work in CFMX8 and BlueDragon. You'd also have to ensure that the values passed don't contain quotation marks and choose a delimiter that works for your application.

This should not be an attempt to obfuscate the code, just make it more difficult to be abused and expire-enabled so that it can't be remotely stored for later posting.

Comment 7 by Chad posted on 4/1/2008 at 2:48 AM

I stick with Lyla captcha - it works great and there is no workarounds I have to deal with and is a breeze to implement. Until CF8 comes up with a better solution I will continue to use it.

Comment 8 by James Moberg posted on 4/1/2008 at 3:22 AM

Concerning Lyla, all Hash References are valid for 30 minutes by default... sometimes it takes me more than 30 minutes to generate a response. (I'd hate to be cflocated back to an empty form because I exceeded a preset time limit.)

Also a persistent scope is required to store the temporary Lyla hash which means that any internal hiccup could result in losing the hash. An encrypted/decrypted method would never be impacted by a server restart, require additional clean-up cycles, or have to work around sticky sessions in a clustered environment.

Comment 9 by Fernando posted on 4/2/2008 at 12:54 PM

Why not a session variable?

After setting the random text variable, we set that variable into a session. Like Session.Captcha=randomText
Is this safe?

If not safe, why not do an Application variable (yes, going crazy).
Create a Structure in an Application variable where you set the IP of the user with the value of the random text.
I, for instance, keep a an Application structure of online users (for 20 minutes) defined by a UUID for each user. I can store it there also and it gets deleted after 20 minutes of idle time from the user or it gets replaced when the user requests another Captcha.
Safer? More insane?

What do you think? What is the safest and easy way to check the Captcha value?

Comment 10 by Raymond Camden posted on 4/2/2008 at 2:47 PM

I avoided session variables in both my guides as I wanted something that could run in _any_ CF application, whether or not sessions were enabled. (Don't forget that sessions are optional.)

Comment 11 by James Moberg posted on 4/2/2008 at 5:18 PM

I agree with Raymond. I also avoid session and application variables if possible so that the process will work in all CF engines and applications without requiring the enabling of additional scopes, url/form parameters and/or dependence on cookies. A date/time-based encoded value basically performs the same exact function without the session/application memory requirements (and problems) and can easily be used over multiple servers (ie, a comment engine that posts to a third-party server.)

Again, if there are any hickups in the server, both session and application variables will be cleared. If they expire within 20 minutes and the user is still composing their message, please ensure that their message is reposted so they don't have to hit their back button. (I usually compose my messages in a separate text editor and then copy-and-paste to the webform in case there are any posting problems and I need to repost.)

Adding the IP is not a bad idea, but some ISPs (like AOL) change the IP address of the user mid-session. I would only integrate IP restrictions to a targeted audience when I know for a fact that their IP addresses will not change (ie, enabling a remotely hosted application to work only for certain employees if they are accessing from the office IP class.)

Comment 12 by sal posted on 4/3/2008 at 2:11 AM

Interesting comments...

But one question on your initial post Ray? I noticed you compared "<cfif hash(form.captcha) neq form.captchaHash>"... by putting a cfimage within a cfform tag does that automagically become part of the form struct? I'm not following where you set the form.captcha?

cheers

Comment 13 by sal posted on 4/3/2008 at 2:12 AM

oops.

nevermind...

/banging head...

sorry man. hah!

Comment 14 by Timothy Farrar posted on 4/3/2008 at 3:36 AM

Ray,

What release of cf8 are you running? I tried the captcha with the word hello. It told me I needed the dimmensions 130 width, and then a height of 35. I entered these dimensions, and it worked. Just want to make sure what I am working on will work on other servers.

Comment 15 by Raymond Camden posted on 4/3/2008 at 4:51 AM

Technically I can't talk about the version of CF I'm running. But I'll test again on my CF8 box. I distinctly remember this odd error issue - where I had to go one more on the width then what the error told me.

Either way - it's a DARN nice helpful message to have.

Comment 16 by Raymond Camden posted on 4/3/2008 at 5:01 AM

sal - glad you got it

Comment 17 by dan plesse posted on 4/7/2008 at 11:25 PM

I just added

<cfimage
action="captcha"
height="60"
width="200"
text="#session.whatWasIt#"
difficulty="low"
fonts="verdana,arial,times new roman,courier"
fontsize="15" />

to my <cfform> and the image did not show. I am using CF 8.0.1 too. Can you believe that. I wrote to Ben Forta too about this.

Code:

<cfformgroup type="vertical" label="Step 4 - Human or Robot check ">

<cfinput type="text" name="captcha_test" value="" size="7" />
<cfimage
action="captcha"
height="60"
width="200"
text="#session.whatWasIt#"
difficulty="low"
fonts="verdana,arial,times new roman,courier"
fontsize="15" />

</cfformgroup> thanks

Comment 18 by Raymond Camden posted on 4/7/2008 at 11:26 PM

If you view source, do you see an <IMG> tag where the captcha should be?

Comment 19 by Hey posted on 4/8/2008 at 4:51 PM

I placed your code in, and on the validation of the form.captcha and the hash: <cfif hash(form.captcha) neq form.captchaHash>

I receive these values:

Form: 8B8C20ABED3E28B960ABD3FC127A83BF

Hash: 5E47596628F59DF0AB12A8C2B9A83278

But I entered in the text in the image correctly?

I guess I'm missing something

Comment 20 by Raymond Camden posted on 4/8/2008 at 5:20 PM

@Hey - not sure what to tell you - do you have it online anywhere?

Comment 21 by asa posted on 4/10/2008 at 2:53 AM

If i use James Moberg/Ray's method to limit a captcha's life, what's to stop a spammer from using the same captcha a million times before it expires? If there is a form field with the captcha hash in it then a spammer could solve it once and then submit the form a million times using the same hashed captcha and solved captcha.

Comment 22 by James Moberg posted on 4/10/2008 at 3:22 AM

What's to stop a spammer? Nothing. But a spambot won't post thousands of posts within a small amount of time in fears that it may raise an alarm or provide too easy of logfile to review.

If you want to add another level of security, you could use an encoded hash and additionally include their IP address into it. This wouldn't stop a spammer from posting to the page multiple times in a short period of time from the same IP, but it would stop the profiling and returning from multiple spambots through proxies and compromised servers.

I've also been using Project HoneyPot to identify compromised IP addresses and post a special message for anyone trying to post from known IPs. Think of it as a DNS blacklist for abusive visitors.
http://en.wikipedia.org/wik...

Comment 23 by James Moberg posted on 4/10/2008 at 3:38 AM

Ray,

I just noticed that when "asa" posted, that the "from" and "return-path" email address on the subscribed message was listed as his. This means that his personal email address has been exposed and that mine is also sent to anyone that subscribes to this post. This also means that bounces will go to him.

There is a problem with this... I have Sender Policy Framework (SPF) configured for third-party mailservers to not accept any email that is not sent from my mail server. To deal with this, please modify your CFMAIL tag as follows:
from="submitter@email.com" failto="blog@coldfusionjedi.com"
... or you could use your email address in both places (from/failto) to protect the privacy of users who post on your forums.

For more on SPF, go to:
http://www.openspf.org/
http://en.wikipedia.org/wik...

Also, for bypassing anti-spam filters, read this:
http://www.list-unsubscribe...
and add the following:
<CFMAILPARAM Name="List-Unsubscribe" Value="<mailto:unsubscribe@email.com>">

Comment 24 by smee posted on 7/8/2008 at 2:23 AM

Everything works fine EXPECT for the

<cfif hash(form.captcha) neq form.captchaHash>
<cfset error = error & "You did not enter the right text. Are you a spammer?<br />">
</cfif>

I have the first part of the section as an include file so I could reuse. When I tried to add it the <cfif hash> statement on the actually page, it breaks. What am I doing wrong. I have done your code on other website and it works great. I did it as an include files. Trying to get it working on the forum is harder.

Comment 25 by Raymond Camden posted on 7/8/2008 at 5:09 AM

Well I'd need to see the entire application. If the original code works for you and your modified code does not, then I'd consider just reverting.

Comment 26 by Superfly posted on 10/24/2009 at 5:02 PM

Ray, James,
thank you for depping it to the best here.

As you call yourself "quick and dirty" these threads, I'd like to submit one that could be safe enough for non higly exposed web sites.

I use CF regular expression validation to control the captcha verification. I think that the fact CF will write the value within a script that add extra characters ('(','/',')') will be *quite* a 1st level protection acceptable method.

The main advantage is that using CF validating JS scripts, the form will not be submitted and we don't have to repopulate values that were previously given by the user, which is quite simple for simple texts inputs (as you did well), but can be a real headache if we have to manage selects, grids or file uploads.

Once again, we have to balance between ease of (re-)use, I choosed ease ... for now (until I got spammed despite of this ?).

I made a little & simple actor (fusebox like) that I simply <cfinclude> in any form I want to be captcha controlled :

*** code bellow ****
<cfprocessingdirective pageencoding="utf-8">
<cffunction name="makeRandomString" returntype="string" output="false">
<cfset var chars = "12345689abdefghjnqrtyABCDEFGHKLMNPQRSTUVWXYZ"><!---do not use both 7 and 1 if you're french or your site intends french users; we are not familliar with '7' without the small horizontal bar in the middle of the vertical part --->
<cfset var length = 5>
<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>

<div id="captcha">

<cfoutput>
<cfset captcha = makeRandomString()>
<span class="required">Anti-spam</span><br />
Please write these characters bellow
<cfimage action="captcha" width="180" height="40" text="#captcha#" fonts="verdana,arial,system" difficulty="medium"><br>
<label for="doloveYourmom">Vérification</label>
<cfinput type="text" name="doloveYourmom" id="ilfautbiencontroler" maxlength="6" style="width:100px;" validate="regular_expression" pattern="(#captcha#)" message="Characters does not match" > <!--- do not name the input field with input or captcha string like. Do not leave this comment either ! --->
</cfoutput>
</div>
*** end of code ***

I've also thought about another solution, but time is missing: Instead of validating the captcha string on submit, we could set up a cfc binded field that will indicate in real time if the input of the user meets the captcha (I.e an image close to the input field that displays red 'non matchning' or green 'matching'). Moreover, this cfc could enable or disable the submit button, regading input and captcha match.
You're the experts ... maybe you could give this a try ?

Again, thanks for this (and the rest) !
Antoine.

Comment 27 by Superfly posted on 10/24/2009 at 5:07 PM

sorry, tranlating issue within the Cfinput field, both ID and Nem should match, as in :
<cfinput type="text" name="doloveYourmom" id="doloveYourmom" maxlength="6" style="width:100px;" validate="regular_expression" pattern="(#captcha#)" message="Characters does not match" >

Comment 28 by Andy posted on 3/15/2011 at 1:33 AM

This is a great tip! one mor question - how do you validate a radial button in this form? thanks,

Comment 29 by jack posted on 5/10/2011 at 3:35 AM

Thank you for this update to your original article! btw, I love your little interstitial jokes! they literally made me LOL!