Using ColdFusion, HTML, And CSS To Stop Spam Form Submissions (Revisited)
For anyone who has posted a comment on my blog recently, you know that I was using a random Math equation to stop automated spamming via form submission. It worked amazingly well; I have only gotten a few spam comments (maybe 5) over the past 6 months or something. Having had such success with that, I thought I would take that idea and make even better so that the user pretty much doesn't have to do anything but submit the form.
This idea, like the previous Math idea, is based on the fact that a user will not see page elements hidden by CSS classes but a spam bot will see them. Since most spam bots do not render the page, they tend to submit all form fields present in the HTML. So, what we need to do is get the spam bot to submit form fields that a user would NOT submit. Furthermore, we need to do this without Javascript 'cause we can't depend on a user to have Javascript.
To get this to work, first I have to come up with a random value:
<!--- Get the de-spamming value. --->
<cfset REQUEST.DeSpam = RandRange( -30, 30 ) />
Then, in the blog comment form, I output this value as an encrypted HEX value:
<input type="hidden" name="de_spam" value="#Encrypt(
REQUEST.DeSpam,
"SECRET_KEY_ONE",
"CFMX_COMPAT",
"HEX"
)#" />
Here, I am using the encryption key "SECRET_KEY_ONE" but you would make something up for your own form (this is a demo only).
The next part of this solution uses two sets of CSS classes for input fields. One set hides the input, the other set does nothing:
/* Hide these inputs. */
input.HideA,
input.HideB,
input.HideC,
input.HideD,
input.HideE,
input.HideF,
input.HideG,
input.HideH,
input.HideI,
input.HideJ,
input.HideK,
input.HideL{
display: none ;
visibility: hidden ;
}
/* Show these inputs. */
input.ShowA,
input.ShowB,
input.ShowC,
input.ShowD,
input.ShowE,
input.ShowF,
input.ShowG,
input.ShowH,
input.ShowI,
input.ShowJ,
input.ShowK,
input.ShowL{}
I am spelling out the CSS class names here so you can follow along, but in the actual solution, the CSS class names are much more random (so that spammers cannot guess things easily).
Then, on the ColdFusion side, I have a comma-delimited list of these two sets:
<!--- Set up the good class list. --->
<cfset lstGoodClasses = "ShowA,ShowB,ShowC,......" />
<!--- Set up the bad class list. --->
<cfset lstBadClasses = "HideA,HideB,HideC,......." />
Then, I pick a random "good" class from the list of visible classes. I take this class and I randomly insert it into a list of bad classes (repeated twice for lower guessing probability):
<!--- Get a randomly chosen good class. --->
<cfset strGoodClass = ListGetAt(
lstGoodClasses,
RandRange( 1, ListLen( lstGoodClasses ) )
) />
<!--- Insert the good class into the bad class. --->
<cfset lstAllClasses = ListInsertAt(
ListAppend(
lstBadClasses,
lstBadClasses
),
RandRange(
2,
(ListLen( lstBadClasses ) * 2) - 2
),
strGoodClass
) />
At this point, the variable "lstAllClasses" contains all but one CSS classes that will hide any input field that it is attributed to. We also have the ONE good class (the only class in the list that shows an input field) in the variable, "strGoodClass". Using this full list of classes, I output a form submission button for each class:
<!--- Loop over the class list. --->
<cfloop index="strClass" list="#lstAllClasses#" delimiters=",">
<cfsilent>
<!---
Check to see if the current class is our good class.
This is the input whose name will contain the valid
encrypted despamming key.
--->
<cfif (strClass EQ strGoodClass)>
<!--- <br>
Encrypt the valid key with a DIFFERENT secret
key than the hidden form field is encrypted with.
--->
<cfset strButtonValue = Encrypt(
REQUEST.DeSpam,
"SECRET_KEY_TWO",
"CFMX_COMPAT",
"HEX"
) />
<cfelse>
<!---
This is a "bunk" submission button. Set a random
fractional value so that we don't accidentally
duplicate the real value.
--->
<cfset strButtonValue = Encrypt(
(RandRange( -30, 30 ) & "."),
"SECRET_KEY_TWO",
"CFMX_COMPAT",
"HEX"
) />
</cfif>
</cfsilent>
<!--- Output the form submit button. --->
<input
name="de_spam_#strButtonValue#"
type="submit"
value="Post Comment"
class="#strClass#"
/>
</cfloop>
Several points to see in the above code:
All the submission buttons are named "de_spam_" PLUS an encrypted value.
All the submission buttons outputted have CSS classes take from the list we created. Remember, this list only had one GOOD CSS class. This means that of all the submission buttons written to the page, only ONE should be rendered visibly on the page (but all will be in the page's source code).
All the encrypted values in the submission button names are encrypted using a different secret key than the original de-spam value. This will make it near impossible to match the hidden form field to the submission button names.
Only one of the outputted submission buttons (the one with the GOOD CSS class) will have an encrypted value (as part of its name) that matches the original de-spam value.
Ok, so what's with all this hubbub? The reason this works is that form submission button values are ONLY submitted as part of the form data if the user clicks on it. That means that the only one of the form submission buttons will be submitted by the user (since only one submission button is visible to the user).
Now that's all on the form side; let's take a look at the ColdFusion form processing side. This is the code I use during the form data validation:
<cftry>
<!--- Get the key count for despamming. --->
<cfset intDeSpamCount = 0 />
<!---
Decrypt the despam value that has been submitted by the
user. Since this value has been submitted as part of a
form field KEY, we have to loop ove the form fields to
find it.
--->
<cfloop item="strKey" collection="#FORM#">
<!--- Check for the proper key prefix. --->
<cfif FindNoCase( "de_spam_", strKey )>
<!---
Only get the decrypted value the first time we
hit this type of form field.
--->
<cfif NOT intDeSpamCount>
<!---
Decrypt the key suffix to get our de spam
test value.
--->
<cfset FORM.de_spam_test = Decrypt(
UCase(
ReplaceNoCase(
strKey,
"de_spam_",
"",
"ONE"
)
),
"SECRET_KEY_TWO",
"CFMX_COMPAT",
"HEX"
) />
</cfif>
<!--- Increment the key count. --->
<cfset intDeSpamCount = (intDeSpamCount + 1) />
</cfif>
</cfloop>
<!---
Decrypt the despam value that we are testing
against. This was the orignal random value that
we generated.
--->
<cfset FORM.de_spam = Decrypt(
FORM.de_spam,
"SECRET_KEY_ONE",
"CFMX_COMPAT",
"HEX"
) />
<!---
Check to see if the two values are the same. Also,
check to make sure that only ONE dynamic de-spam field
was submitted. If they are more than one, then it was
an automated SPAM submission.
--->
<cfif (
(FORM.de_spam NEQ FORM.de_spam_test) OR
(intDeSpamCount NEQ 1)
)>
<cfset REQUEST.FormErrors.Add(
"Please submit the form by clicking on the 'Post Comment' button"
) />
</cfif>
<!--- Catch any despam errors. --->
<cfcatch>
<cfset REQUEST.FormErrors.Add(
"Please submit the form by clicking on the 'Post Comment' button"
) />
</cfcatch>
</cftry>
Here I am getting the hidden form field value as well as the submit button value, decrypting them, and comparing them. Notice that I have to loop over the FORM collection to get the dynamic submission button keys. We only decrypt the first value that we come across. However, we keep track of the number of dynamic submission values passed in. The idea here is that only a SPAM bot would submit more than one and a standard user would submit ONLY the one that they clicked on.
I only put this into effect this today, so I don't know how effective it is, but so far no spam has gotten through (except for three comments that got posted in the short time while I was working out the kinks). In order for a SPAM bot to circumvent the spam protection, they would either have to decrypt both the form values or randomly submit the correct form button. Since the de-spam value changes every page run, this becomes unlikely. I suppose the spam bot could submit the form data multiple times with different form fields excluded... but that's a problem I will tackle when I get to it.
So now, no more math questions. No more CAPTHA text. The only down side is the user has to physically click the submit button... but that's not so bad, is it? Oh and their browser has to support CSS... but if it doesn't do that - yikes!!!
Want to use code from this post? Check out the license.
Reader Comments
You write:
<tt>The only down side is the user has to physically click the submit button... but that's not so bad, is it? </tt>
Well, kinda. You can submit forms by hitting "enter" in just about any field other than a textarea. I think there are enough keyboarders (like myself) who would submit forms like that.
Does the user get any indication that their entry was not accepted? I waffle on whether they should or not. If you tell them, the spammers can fine-tune, which is bad. If not, you end up ignoring your users, which is far worse -- unacceptable for many projects.
Thoughts?
Always listen to experts. They'll tell you what can't be done and why.
Then do it.
Robert Heinlein (1907 - 1988)
Fellow developer, I completely agree with you. This was one implementation and is, in fact, not the one that I use any more. I use the keyboard quite heavily and this method was hurting my usability (let alone anyone else's).
What implementation are you using instead?