Enhancing Request Forgery Protection - Security Series #9.1

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.

  1. 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.
  2. 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()) />
    
    <cfoutput>
    <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" />
    </form>
    </cfoutput>

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.


    <cfif
        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">
    <cfelse>
        <cfset StructDelete(session.testForm, "token")>
        <cfset StructDelete(session.testForm, "tokenExpires")>
    </cfif>

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).

Comments
dt's Gravatar http://owasp-esapi-java.googlecode.com/svn/trunk_d...

>> Note that in addition to defeating all forms of parameter tampering attacks, there is a side benefit of the AccessReferenceMap. Using random strings as indirect object references, as opposed to simple integers makes it impossible for an attacker to guess valid identifiers. So if per-user AccessReferenceMaps are used, then request forgery (CSRF) attacks will also be prevented.

just throwing that in there.
# Posted By dt | 2/9/09 12:52 PM
Jason Dean's Gravatar @dt, Yeah, I read about that recently in my research for the CF ESAPI project. I have started building the AccessReferenceMap for CF ESAPI, but that is a considerably more complicated solution than what I am proposing here. What I am proposing here is more target toward those that would like to put something in place quickly or into an existing application.

Additionally, ESAPI still implements the use of a CSRF token, so I am not sure if the AccessReferenceMap can really solve all problems. That is something I am still learning. We need to talk more about the AccessReferenceMap, because I am still a little fuzzy on where and how it should be used.
# Posted By Jason Dean | 2/9/09 1:03 PM
Rick O's Gravatar I've traditionally done form tokens on a form-by-form basis, but I'd have to wonder if it might not be a good idea to have a Session.formTokenPool FIFO array that can only get so big, but handles all session form tokens for the application. For a blog with comment/contact forms you might limit it to 1 token, for a small CRUD app maybe 2, and for a large app with lots of forms maybe 3 or 4.

Because, really, while I get the "the user might have several windows open" argument, how practical is this for a single app? If the use-case is that if the user's token isn't found in their pool then they just get prompted to resubmit to verify their info, then is that so bad?

This way, you don't have 15 different tokens for 15 different forms all at the one time, and each form doesn't have to worry about its own token -- you're elevating the security from form-level to session-level. If you wanted to get really complex, you could even make it a per-user setting. If you know someone is always exceeding their pool size and constantly getting asked for verification, punch them up a notch.

Just thinking out loud. You've brought up some good points, as always.
# Posted By Rick O | 2/9/09 1:39 PM
O?uz Demirkap?'s Gravatar Hi Jason,

Thanks for the post. I think the first section of the code should have

cfset session.testForm.token = key

The other issue is the Acunetix scanner still complains as 'HTML form without CSRF protection'.

Any idea?
# Posted By O?uz Demirkap? | 8/16/12 12:19 PM
Jason Dean's Gravatar What does Acunetix check for in a form? Is it looking for a token with a particular name? Can you configure what field it should be looking for?
# Posted By Jason Dean | 8/16/12 1:34 PM
O?uz Demirkap?'s Gravatar Here is the result: http://pastebin.com/fMMU7Fsz
# Posted By O?uz Demirkap? | 8/16/12 2:04 PM
Jason Dean's Gravatar I'm gonna guess that it just doesn't recognize the field "token" as a CSRF token. Try renaming it to CSRFToken and see if that changes anything.
# Posted By Jason Dean | 8/16/12 3:49 PM
O?uz Demirkap?'s Gravatar It looks like CSRFToken solved the issue!

Great! Thanks! :)
# Posted By O?uz Demirkap? | 8/16/12 3:59 PM
Michael Sharman's Gravatar We handle this a little differently, we still use sessions but instead of storing keys in each users session scope we use a one-way hash() for comparison.

https://github.com/michaelsharman/CsrfProvider

The idea is that the application has a "secret", then each form has an "intention" (basically a unique string that represents the form), then we have the users session.sessionId. By hashing those 3 values, you can have CSRF protection without storing anything in the session scope.

We load a hidden form field:

#CsrfProvider.renderToken(intention="myFormName")#

Then on form submission we verify:

#CsrfProvider.verifyToken(intention="myFormName", token=form._token)#
# Posted By Michael Sharman | 10/22/12 1:24 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner