Skip to content

Instantly share code, notes, and snippets.

@JamoCA
Last active February 5, 2020 18:57
Show Gist options
  • Save JamoCA/c4e83ca402bd6175a1d7 to your computer and use it in GitHub Desktop.
Save JamoCA/c4e83ca402bd6175a1d7 to your computer and use it in GitHub Desktop.
ColdFusion UDF for Google reCAPTCHA v2.0 - Works with CF9. Support for CFX_HTTP5.
<!--- site defaults/override --->
<CFSET Request.API = {
recaptcha = {
enabled = 0,
sitekey = "",
secret = ""
}
}>
<CFSET useRecaptcha = StructKeyExists(Request.API, "recaptcha") AND StructKeyExists(Request.API.recaptcha, "enabled")
AND VAL(Request.API.recaptcha.Enabled) AND LEN(Request.API.recaptcha.sitekey)>
<!doctype html>
<html lang="en">
<head>
<title>Untitled</title>
<script src="https://www.google.com/recaptcha/api.js" type="text/javascript"></script>
</head>
<body>
<CFIF CGI.Request_method IS "post">
<CFIF Request.isRecaptchaGood()>
<p>Success! Good Recaptcha</p>
<CFDUMP VAR="#Form#">
<!--- do things --->
<CFEXIT>
<CFELSE>
<p>Bad Recatcha</p>
</CFIF>
</CFIF>
<CFOUTPUT>
<form action="#CGI.Script_Name#" method="post">
<CFIF useRecaptcha>
#Request.makeRecaptcha()#
</CFIF>
<button type="submit">Continue</button>
</form>
</CFOUTPUT>
</body>
</html>
<!--- 2/4/2016 isRecaptchaGood
by SunStar Media
Requires this Recaptcha v2.0 JS in the HEAD:
<script src="https://www.google.com/recaptcha/api.js" type="text/javascript"></script>
Requires this to be on the form (at a minimum):
<div class="g-recaptcha" data-sitekey="#Request.Api.recaptcha.SiteKey#"></div>
To validate (if not configured properly, will return true):
isRecaptchaGood(); /* Recommended usage. Uses form parameter, Request.Api.Recaptcha.secret & CGI.Remote_Addr */
isRecaptchaGood(Form["g-recaptcha-response"]);
isRecaptchaGood(Form["g-recaptcha-response"], RecaptchaSecret);
isRecaptchaGood(Form["g-recaptcha-response"], RecaptchaSecret, CGI.Remote_Addr);
--->
<cffunction name="isRecaptchaGood" returntype="boolean" output="no">
<cfargument name="Response" type="string" required="No" default=""><!--- Will default to FORM["g-recaptcha-response"] if exists --->
<cfargument name="Secret" type="string" required="No" default=""><!--- Will default to Request.Api.recaptcha.secret if exists --->
<cfargument name="RemoteIP" type="string" required="No" default="#CGI.Remote_Addr#">
<cfset var request_url = "https://www.google.com/recaptcha/api/siteverify">
<cfset var APIResponse = 1>
<cfset var httpResponse = "">
<cfif not LEN(trim(Arguments.Response)) AND StructKeyExists(Form, "g-recaptcha-response")>
<cfset Arguments.Response = FORM["g-recaptcha-response"]>
</cfif>
<cfif not LEN(trim(Arguments.Secret)) AND isDefined("Request.Api.recaptcha.secret")>
<cfset Arguments.Secret = Request.Api.recaptcha.secret>
<cfelseif not LEN(trim(Arguments.Secret))>
<cfset Arguments.Secret = ""><!--- option: set to global default --->
</cfif>
<cfif isdefined("Request.API.Recaptcha.Enabled") AND NOT YesNoFormat(Request.API.Recaptcha.Enabled)>
<cfset APIResponse = 1><!--- not enabled; return success --->
<cfelseif not len(trim(Arguments.Secret))>
<cfset APIResponse = 1><!--- secret parameter not configured; return success --->
<cfelseif not len(trim(Arguments.Response))>
<cfset APIResponse = 0><!--- enabled, but empty response? Reject & do not perform API request --->
<cfelse>
<cfset request_url = request_url & "?secret=" & trim(Arguments.Secret) & "&response=" & trim(Arguments.Response) & "&remoteip=" & trim(Arguments.RemoteIP)>
<!--- Depending on version of ColdFusion used, HTTPS requests may not work due to unfixed bugs.
Recommend using CFX_HTTP5 (better DNS time-to-live support & compatible with wider variety of SSL certs.)
<cfhttp url="#request_url#" port="443" METHOD="get" USERAGENT="#CGI.Server_Name# reCaptcha" getAsBinary="never"></cfhttp>
<CFSET httpResponse = CFHTTP.FileContent>
---->
<CFX_HTTP5 URL="#request_url#" METHOD="GET" OUT="httpResponse" SSL="5" HEADERS="User-Agent: #CGI.Server_Name# reCaptcha" MAXTIME="15000" REDIRECT="N" SSLERRORS="OK">
<cfif isJson(httpResponse)><!--- Check response for {"success":true} --->
<cfset httpResponse = deserializeJSON(httpResponse)>
<cfset APIResponse = isStruct(httpResponse) AND StructKeyExists(httpResponse, "success") AND YesNoFormat(httpResponse.success)>
<cfelse>
<cfset APIResponse = 1><!--- Downtime happens; Possible API Problem; Temporary success --->
</cfif>
</cfif>
<cfreturn YesNoFormat(APIResponse)>
</cffunction>
<!--- 2/4/2016 makeRecaptcha (https://developers.google.com/recaptcha/intro)
by SunStar Media
Requires this Recaptcha v2.0 JS in the HEAD:
<script src="https://www.google.com/recaptcha/api.js" type="text/javascript"></script>
To generate form (if not configured properly, will return true):
isRecaptchaGood(); /* Recommended usage. Uses form parameter, Request.Api.Recaptcha.secret & CGI.Remote_Addr */
--->
<cffunction name="makeRecaptcha" returntype="string" output="no">
<cfargument name="sitekey" type="string" required="No" default=""><!--- Will default to Request.Api.recaptcha.sitekey if exists --->
<cfargument name="secret" type="string" required="No" default=""><!--- Will default to Request.Api.recaptcha.secret if exists --->
<cfset var temp = {}>
<cfif not LEN(trim(Arguments.sitekey)) AND isDefined("Request.Api.recaptcha.sitekey")>
<cfset Arguments.sitekey = Request.Api.recaptcha.sitekey>
</cfif>
<cfif not LEN(trim(Arguments.secret)) AND isDefined("Request.Api.recaptcha.secret")>
<cfset Arguments.secret = Request.Api.recaptcha.secret>
</cfif>
<cfset temp.HTML = "">
<CFIF LEN(Arguments.sitekey)>
<cfset temp.HTML = '<div class="g-recaptcha" data-sitekey="#Arguments.sitekey#"></div>'>
</CFIF>
<cfif len(Arguments.secret)>
<cfset Temp.ts_ms = dateDiff("s", dateConvert("utc2Local", "January 1 1970 00:00"), now()) * 1000>
<cfset Temp.session_id = createUUID()>
<cfset temp.JSON = '{"session_id": "#Temp.session_id#", "ts_ms": #Temp.ts_ms#}'>
<cfset temp.keyHash = hash(Arguments.secret, "SHA", "UTF-8")>
<cfset temp.keyBinary = binaryDecode(temp.keyHash, "hex")>
<cfset temp.keyAES = []>
<cfloop from="1" to="16" index="thisSlice">
<cfset ArrayAppend(temp.keyAES, temp.keyBinary[thisSlice])>
</cfloop>
<cfset temp.keyBase64 = binaryEncode( javacast("byte[]", temp.keyAES), "base64")>
<cfset temp.sToken = Encrypt(temp.JSON, temp.keyBase64, "AES/ECB/PKCS5Padding", "base64")>
<cfset temp.sToken = Replace(temp.sToken, "=", "", "ALL")>
<cfset temp.sToken = Replace(temp.sToken, "+", "-", "ALL")>
<cfset temp.sToken = Replace(temp.sToken, "/", "_", "ALL")>
<cfset temp.HTML = '<div class="g-recaptcha" data-sitekey="#Arguments.sitekey#" data-stoken="#temp.sToken#"></div>'>
</cfif>
<!---
<cfsaveContent variable="temp.dump"><cfdump var="#Temp#"></cfsavecontent>
<cfset temp.HTML = temp.HTML & temp.dump>
--->
<cfreturn temp.HTML>
</cffunction>
@asugizaki
Copy link

asugizaki commented Dec 1, 2019

I'm using your code to implement reCaptcha and it's working great!
The only part I'm stuck on is getting it to work when it's rendered/sent via AJAX.
The comment form has things like email, name, comments, and the captcha.
What normally happens is when the comment is submitted, it does an AJAX call to get processed and if successful it adds the comment to the database and returns a JSON which includes the new captcha otherwise the existing captcha is still there with it's completed state.
I need to either reset that state so a new comment with new captcha can be set or render a new captcha on successful comment post so a new one can be done.
Also how do I get this to work with multiple captchas on the same page?
Do you have any insight on this and how to get it to work?

@JamoCA
Copy link
Author

JamoCA commented Dec 3, 2019

To support your workflow, I think you may need to use a javascript-only solution to validate the reCaptcha prior to posting the user's payload to your ajax endpoint. Here's a similar question on StackOverflow regarding supporting multiple reCaptcha's on a single page:
https://stackoverflow.com/a/28126317/693068

To respect user privacy, we don't use reCaptcha anymore. We using ColdFusion with StackPath. Their WAF performs a lot of user testing and automatically adds passive CSRF protection to all of our forms. (StackPath also provides R/DDoS protection and is a lot less expensive than Cloudflare if you have multiple web applications.) We also block all HTTP1.0 form submissions. Both of these updates have dramatically reduced the amount of fraud we've experienced in the past.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment