Sunday, July 29, 2012

Technique For Adding Basic CSRF Protection in a FW/1 Application

Just over two years ago, I wrote a blog post about how I protected my web applications from cross-site request forgery (CSRF) attacks using Model-Glue's event types feature.  Recently, I've started building ColdFusion applications using the Framework One (FW/1) MVC framework, so I needed to come up with a new approach to try and block CSRF attacks. Ideally, I wanted to do it in such a way that once I had my CSRF protection set up, I didn't have to think about it anymore, that I wouldn't have to remember to add one or more lines of code to each action I wanted to safeguard (and risk overlooking an action or two).

First, a refresher on what CSRF is: it's an attack where the hacker is counting on the user still being logged in/authenticated to the web application he's targeting.  The hacker creates a web page that posts data to the target application using a known URL or form submission destination and lures the user to that page.  When the user triggers that page, whatever data that page sends to the target application is accepted because the target application sees it as a valid request from a valid user.  It's a serious vulnerability, which is why ColdFusion 10 comes with new functions designed specifically to help block CSRF and why those functions were included in the CFBackport project (an open-source project that makes some ColdFusion 10 functions available to ColdFusion 8 and 9).

The basic way to prevent CSRF attacks (or at least make them far less effective) is to generate a unique value that is stored in the user's current session, include that value in the URL or form submissions you want to protect, and then match the value in the URL or form submission to the value stored in session and make sure they match before allowing the action to proceed.  So unless the hacker can figure out what that unique value currently is for a particular user, they cannot mimic it in their fake web page and data from that page won't be accepted.

So first I added code to my controller function in charge of validating user authorization that would create a token variable in the session:

...
session.user= arguments.rc.user;
session.token= CreateUUID();
....


At this point, I could have manually started adding the token variable to URL strings or placed it in hidden form fields in my forms, but again I wanted my CSRF to be as "automatic" as possible.

In FW/1, the best practice for creating URLs for hyperlinks and form submissions within the application is to use the buildUrl() function built into the framework.  With buildUrl(), the following line of code:

<form name="addItem" method="post" action="#buildUrl(action='item.add',queryString='foo=2')#" >

 

...gets rendered as (assuming you've stuck with the FW/1 defaults)...

<form name="addItem" method="post" action="index.cfm?action=item.add&foo=2" >


The buildUrl() function lives inside the framework.cfc file that is the heart of FW/1.  Implementing the FW/1 framework in your application is as simple as changing your Application.cfc to extend that framework.cfc file.

I wanted buildUrl() to automatically incorporate my session.token (if it existed) in the resulting URL. So to do that, I created a subclass of framework.cfc called frameworkExt.cfc with the following code:

component extends="framework" {
    public string function buildURL( string action = '', string path = variables.magicBaseURL, any queryString = '', string tokenKey= "token") {

        //check for presence of tokenKey in session
        if(StructKeyExists(session,tokenKey)) {
            if(isStruct(arguments.queryString)) {
                arguments.queryString[arguments.tokenKey]= session[tokenKey];

            } else {

                if(arguments.queryString== "") {

                     //If the action has the url query elements hard-coded (which means queryString should be empty), append to that
                     if(ListLen(arguments.action,"?") GT 1) {
                         arguments.action &= "&#arguments.tokenKey#=#session[tokenKey]#";
                     } else {
                         arguments.queryString= "#arguments.tokenKey#=#session[tokenKey]#";
                     }

                } else if(ListLen(arguments.queryString,"?") EQ 1) {
                    arguments.queryString &= "?#arguments.tokenKey#=#session[tokenKey]#";

                } else {
                    arguments.queryString &= "&#arguments.tokenKey#=#session[tokenKey]#";
                }

            }
        }

        return super.buildUrl(arguments.action,arguments.path,arguments.queryString);
    }
}


Basically, I created a new version of buildURL() that acts as a kind of pre-processor to the original buildURL() function. If session.token exists, it will get incorporated into the URL just as if I had explicitly submitted it to buildURL() in either the action or queryString parameter, then transformed into the final URL by the original function in the super class.

(Some caveats here...I don't know for certain if this technique would work for every use case of buildURL(). It's a complex function capable of creating URLs in a number of different formats in order to produce SES-style links and point to subsystems. But I would think that since I'm simply adding in another URL variable that it ought to work in every case. Also, any time you write a function that modifies a framework function (even via extension) you run the risk of that framework function changing in a future version, potentially breaking your modification).

Once I changed my Application.cfc to extend my frameworkExt.cfc instead of framework.cfc and reloaded my application, the session token became part of all of the URLs I visited or invoked as an authenticated user.

The final step was to add code that would check the token value submitted in the URL or form against the value of session.token, and if it didn't redirect the request to an error or login page.  In Model-Glue, I could add that check as part of a message broadcast in an event type, and then apply that event type to any request where I wanted to enforce CSRF protection.  FW/1 doesn't have event types, but it has something similar:  in an FW/1 controller, you can create before() and after() functions that execute code (you guessed it) before and after the processing of each function in that controller.  So if I wanted to check the submitted token against the session token before every page request that invoked a function in my "widgets" controller, I could put that code in the before() function.

That would have been the conventional way to go about it, but I decided to do it a bit differently in my application.  Since I wanted to add CSRF security to any action undertaken by an authenticated user, and all of my unauthenticated requests were handled by a small set of controllers, I decided to do a conditional check on the session token in the setupRequest() function in Application.cfc (setupRequest() being a function inherited from framework.cfc that runs at the start of each request):

function setupRequest() {

    if(!ListFind("main,auth",getSection())) {
        controller("auth.checkSessionSecurity");
    }

    if(StructKeyExists(session,"user")) {

        if(!StructKeyExists(rc,"token") OR rc.token NEQ session.token) {
            controller("main.notAuthorized");
        }
        rc.user= session.user;
    }
    ...
}


The FW/1 getSection() function returns the section value of the request (in FW/1, the section equates to / matches up with a controller CFC of the same name), so the first if statement basically states that unless the request is invoking the main or auth controller, the action should redirected to the checkSessionSecurity() function of my auth.cfc controller, which in this application makes sure the user has authenticated and a user object exists in session.

If a user object does exist in session, that means session.token should exist (since it's added to the session at the same time the user object is). So the nested if statement verifies that a token variable was submitted via URL (and hence appears in FW/1's rc struct) and that its value equals that of session.token, and if either of those conditions fails then the user is kicked out to an error page and prevented from executing the original request.

So with this setup, I get protection from CSRF attacks simply by using buildURL() as normal and putting all of my sensitive controller functions in controllers other than main or auth.

Some Additional Notes

  • The drawback to having URLs in your application that change with every user session is that it can interfere with behavior testing using automation tools like Selenium IDE, because any token value captured during the creation of your testing script will be invalid the next time you run them.  One way you can get around this is to add conditional logic to the code that that determines if the application is running in your local development environment (your own machine or a test server), and if so sets the session token value to the same value each and every time (regardless of user session), giving you an unchanging URL in your test environment that you can record in automation scripts.

  • If your application has access to very sensitive personal or financial information, you may want to take your CSRF security a step further.  Some applications rewrite the session token value every few minutes instead of leaving it the same during the user's authenticated session.  You should be able to modify the technique described in this post to do that if you feel it's warranted.