This is a follow up to my previous post on Request Forgery Countermeasures
At a recent ColdFusion Meetup I was presenting on Application Security: Beyond SQL Injection and Wil Genovese pointed out that my current request forgery protection has a small flaw in it. That flaw is that if the form is loaded but never submitted, the token that is created for the form is still active and could be used for a request forgery attack. While this is a relatively small attack vector, it can still be made smaller.
A couple of points first.
- In my countermeasure, each form gets its own CSRF Token, so a unused token could only be used against that form, not against just any form.
- I store the token in the session, so the token is only good for as long as the session is. This reduce the vulnerability window even more.
That said, let's make it a little more difficult for would be hackers. Since a session could persist indefinitely (as long as the user continues using the application or if the session is set no tto time out for a long time), an unused CSRF token could remain active for hours, or days. This is a problem. I only want to leave my token active for as long as it takes the user to fill out the form, and if they never hit submit, I want that token to be invalidated.
So here is what we are going to do.
<cfset session.testForm = StructNew() />
<cfset sr = createObject("java","java.security.SecureRandom") />
<cfset key = sr.nextLong() />
<cfset session.testForm.tokenExpires = DateAdd("s",600,Now()) />
<form action="test2.cfm" method="post">
<input type="text" name="token" value="#session.testForm.token#" /><br />
<input type="text" name="testField" value="" /><br />
<input type="submit" name="btnSubmit" value="Submit" />
Here, we are creating a struct to hold our CSRF token, and we are also adding an expiration time for token. It will expire 600 seconds (10 Minutes) from the time it was created. Of course, you can set this time to any amount you want. For longer forms you may want to set it longer.
Now we can put logic into our receiving page/function to tell the application not to accept the token if it is expired.
NOT StructKeyExists(form, "token") OR
NOT StructkeyExists(session.testForm, "token") OR
NOT StructkeyExists(session.testForm, "tokenExpires") OR
NOT IsDate(session.testForm.tokenExpires) OR
NOT session.testForm.token EQ form.token OR
NOT DateDiff("s",Now(),session.testForm.tokenExpires) GT 0
<cflog file="Security" type="warning" text="Possible CSRF Attack: Include CGI Info Here">
<cfthrow message="Access denied">
<cfset StructDelete(session.testForm, "token")>
<cfset StructDelete(session.testForm, "tokenExpires")>
If any of a number of conditions fail, including the lack of a token, an expired token, or a non-date token expiration, the application will log a message and throw an error. But if everything passes, then the token and expiration are deleted and the processing continues.
This is still not a "perfect" solution, as there is a small window of opportunity for an attack. But if this is combined with proper XSS protection and an SSL connection, then I think it is about the strongest solution we can get considering what we have to work with (HTTP).