ColdFusion 10 backport: CSRF functions
This time, in my series of posts backporting features from ColdFusion 10 to older versions, I’m tackling CSRFGenerateToken() and its partner in crime CSRFVerifyToken().
What do they do?
CSRF = Cross Site Request Forgery. For a full explanation on CSRF you can head over to Wikipedia. But here’s the gist of what happens with these functions in their normal expected usage:
- Grab the returned token from CSRFGenerateToken().
- Behind the scenes, ColdFusion hides the token away in the session.
- Put the token into a hidden form field.
- When the form submission is received, pass the form token to CSRFVerifyToken().
- If it matches the one stored in the token, process the form.
- If it doesn’t match, chances are someone is forging a request so throw it away.
Options
Key : When generating your token you can provide it a key to identify it. This means you can have multiple tokens stored for different forms, very useful if your page have multiple forms as you won’t know which might be submitted. Random : The other option in generation is to randomise the token. Without randomisation, each time you generate a token with the same key, it’ll always return the same one (randomly generated once, per key, per session lifetime).
The backport
Storing the CSRF tokens
In ColdFusion 10, the CSRF tokens are stored in the Session according to the documentation. But doing a CFDump
of the session scope won’t show anything, so I’m guessing they’re hidden internally somehow, similar to other session information like the creation time. Obviously we can’t just magic a hidden bit of the session, for storing tokens, in older versions of ColdFusion. Which means we’ll just have to store them in the scope itself. I’d definitely recommend you pretend the tokens are invisible, because they won’t be there if / when you upgrade to ColdFusion 10. My choice for storage is session._cfbackportcsrf = StructNew()
. A structure makes sense as we need to support multiple tokens, identified by keys.
Generating the tokens
The current defined behaviour in CF10 creates a 20-byte (40 character) hexadecimal token. They appear to be randomly generated and must be designed to avoid guessing subsequent tokens to increase security. Using CreateUUID
would probably make sense but you need to consider performance issues with ColdFusion 8 and below, so we’ll restrict that to ColdFusion 9 only. For older versions we’ll have to rely on RandRange
using the SHA1PRNG
algorithm (IBM WebSphere installs will have to use IBMSecureRandom instead). Either way, both will have to be extended to meet the 20-byte requirement… especially since RandRange
doesn’t seem happy when called with: RandRange(0, 1461501637330902918203684832716283019655932542975)
;)
<cfscript>
if (ListFirst(server.coldfusion.productVersion) Gte 9) {
token = "";
for (i = 1; i Lte 2; i = i + 1) {
num = FormatBaseN(RandRange(0, 65535, "SHA1PRNG"), 16);
token = token & RepeatString("0", 4 - Len(num)) & num;
}
token = token & Replace(CreateUUID(), "-", "", "all");
} else {
token = "";
for (i = 1; i Lte 10; i = i + 1) {
num = FormatBaseN(RandRange(0, 65535, "SHA1PRNG"), 16);
token = token & RepeatString("0", 4 - Len(num)) & num;
}
}
UCase(token);
WriteOutput(token);
</cfscript>
Note that according to the documentation for RandRange
states that large positive (or negative) numbers might result in poor randomness. Which is why I stuck with generating 2 bytes at a time. With CreateUUID
, after we remove the “-” characters we get 16 bytes so we still needed to throw another 4 into that token.
Putting it all together
<cffunction name="CsrfGenerateToken" output="false" returntype="string">
<cfargument name="key" type="string" required="false" default="" />
<cfargument name="random" type="string" required="false" default="false" />
<cfscript>
var lc = StructNew();
// TODO: Session locking?
if (Not StructKeyExists(session, '_cfbackportcsrf')) {
session['_cfbackportcsrf'] = StructNew();
}
if (arguments.random Or Not StructKeyExists(session._cfbackportcsrf, arguments.key)) {
if (ListFirst(server.coldfusion.productVersion) Gte 9) {
lc.token = "";
for (lc.i = 1; lc.i Lte 2; lc.i = lc.i + 1) {
lc.hex = FormatBaseN(RandRange(0, 65535, "SHA1PRNG"), 16);
lc.token = lc.token & RepeatString("0", 4 - Len(lc.hex)) & lc.hex;
}
lc.token = lc.token & Replace(CreateUUID(), "-", "", "all");
} else {
lc.token = "";
for (lc.i = 1; lc.i Lte 10; lc.i = lc.i + 1) {
lc.hex = FormatBaseN(RandRange(0, 65535, "SHA1PRNG"), 16);
lc.token = lc.token & RepeatString("0", 4 - Len(lc.hex)) & lc.hex;
}
}
lc.token = UCase(lc.token);
session._cfbackportcsrf[arguments.key] = lc.token;
} else {
return session._cfbackportcsrf[arguments.key];
}
return lc.token;
</cfscript>
</cffunction>
<cffunction name="CsrfVerifyToken" output="false" returntype="boolean">
<cfargument name="token" type="string" required="true" />
<cfargument name="key" type="string" required="false" default="" />
<cfscript>
// TODO: Session locking?
if (StructKeyExists(session, '_cfbackportcsrf')
And StructKeyExists(session._cfbackportcsrf, arguments.key)
And session._cfbackportcsrf[arguments.key] Eq arguments.token) {
return true;
}
return false;
</cfscript>
</cffunction>
Github
As always I’ve put these functions into my CFBackport project, along with all the other functions that I have replicated in older versions of ColdFusion.
Feedback
It’d be really interesting to hear back from anyone who is thinking of using this or any of the other functions I’ve recreated. Especially if there are other new features you’d love to see available in older versions.