« At Large: What to do about ATM fees | Main | Is this the right time for the U.S. to switch to the metric system? »

An easy filter for cross-site request forgeries (CSRF)

I just checked off one of the requirements for one of my projects at work: protect from cross-site request forgeries, or CSRF. It was much easier to accomplish than I expected-- here are the details.

CSRF is the technique of creating a URL or form post on one site which points to another site and is either automatically (via image source or JavaScript) or manually called. The idea is to hope that the user's browser is logged into this other site and will request the URL that's been crafted for it. Imagine if you're logged into Ebay while browsing another, less trustworthy site, and someone slips code into a user-generated product review which causes your browser to submit a request to buy their item.

To prevent this kind of attack from targeting your own site, you can require that a session-based FORM or URL token be passed with each request. If it doesn't exist or doesn't match what you last sent down to the browser, then you'll know that the request didn't originate from your site. So first, I dropped some code into the onRequestStart function of my Application.cfc file which creates a new token for each page reqest and stores it in session:


<!--- If a token doesn't exist in the form scope --->
<cfif not IsDefined("FORM.token")>
     <!--- Create one and store it in session --->
     <cfset FORM.token = '' />
     <cfset SESSION.token = CreateUUID() />
</cfif>
<cfif IsDefined("FORM.fieldNames") and FORM.token neq SESSION.token><!--- Else if a token does exist, but it doesn't match what's in session --->
     <!--- Abort all page processing --->
     <cfabort />
</cfif>

Some of you might wonder why I don't use a cfparam instead of checking whether the form token exists; the answer is that I need to check whether that variable exists so I know whether to create a new value in session. So what now? Just embed the token in your forms:


<form action="anypage.cfm" method="post">
<input type="hidden" name="token"" value="#SESSION.token#" />
...
</form>

This will be enough to stop all CSRF attacks, at least for my form-based site, in seven lines of code. The code makes a new session token for each request, except when one is submitted. If the token doesn't exist or doesn't match, the page is aborted. Now, if your site allows actions based on query string variables, or even if you just call a specific URL (e.g., calling "deleteAccount.cfm" deletes your account), you'll want to modify the code to also 1) pass the token in each link in the site, and 2) look for the token in the query string as well as in the form scope. Has anyone else found a different technique?

[Note: This posting was altered to prevent page processing from proceeding when a form token was not passed. Thanks to Jason Dean for pointing out this important mistake!]

Comments (16)

Pretty slick!

I usually check the CGI vars on the form action page to make sure the post is coming from the correct domain name. If not, the I don't process the info. This has really come in handy in cutting down contact form spam on some of my sites.

@Tom, I don't see how this can stop all request forgeries. Maybe I am missing something, but let me offer an example of what I am seeing. You placed your block of code into your onRequestStart(), so it will run at the top of every request, that part I get. You are also checking to make sure that the session token matches the form token (when a form token is present), that is good and it makes sense to me. But if the form token doesn't exist, all that is happening is setting a new token to the session variable and then allowing the processing to continue.

So let's say I have a deletePage.cfm file (I usually use procedural examples cause more people get them) that accepts a pageID so it knows which page to delete. If a hacker tricks a user into viewing a page that contains a request forgery that performs the request "http://domain.com/deletePage.cfm?pageID=1", then your block will see that the form scope is missing the token, so it will set one to the session, but then processing will continue and regardless of what other controls you have in deletePage.cfm, the action will work because the request is coming from a legitimate user.

Again, maybe I am missing something, please set me straight if I am.

@Eric, you should not rely on CGI variables like HTTP_HOST or HTTP_REFER to control access or prevent request forgeries. These values can be spoofed using things like cfheader requests (or any language equivalent). Read more about it here: http://tinyurl.com/8r5abb

@Jason, thanks for pointing out the (glaring) mistake with my original code-- if no form token is submitted, then the processing is allowed to proceed. I've corrected it in the article above. You also ask a good question about why my code would protect against all request forgeries-- and the answer is that it does for my site, and with a little modification, it would protect anyone's site, even for query strings. But you make a good point that my article doesn't explain very well that you'd need to customize the solution for each of your own sites.

The reason that my own site is protected is because all of the actions are form-based, and every form contains a token. If someone allowed actions to occur just by calling a page, with or without a query string, then you would need to embed the token in each of the links in your page.

@Tom, I'm sorry, but I don't think what you added changes anything. Users could still be tricked into submitting a POST request that does not include the token field and the processing would still go through. If form.token is not defined, then processing will continue.

I think what you may want to do instead is create the token onSessionStart() and place it in the session scope and then require the token in onRequestStart() and compare it with what is in the session, if they don;t match, then abort.

Personally, I would not want to deal with that. Having EVERY request to every page be a POST request sound onerous at best, but it sounds like that is the way you have it set up.

Again, I may be missing something. Not trying to be a jerk.

Also, I just noticed, doing it this way means that you need to make sure that all of your actions (whether action pages or methods) need to make sure that all incoming variables are scoped, or you will have a huge security hole.

You are only checking to see if the form fields exist, what if the variables are passed in on the URL string? Does your action page/method check to make sure that PageID is form.pageid?

Perhaps you already thought of that, but I wanted to point that out, just in case.

Jason, again this isn't meant to be one-size-fits-all code. It works for form submissions and can be easily modified for query strings. I don't think that there's a security hole for other variables, though, since third-party sites can only cause you to send customized URLs or form data. They can't modify your browser headers or cookies. Am I missing another way for third-party sites to affect the information you send in a request?

Also, you're right again that my fix didn't solve the problem... I just don't know why I wasn't seeing the problem at first. [sigh] I've tested another solution with two conditional blocks and modified the entry above to show it, so let me know what you think.

@Tom. If you application is 100% POST requests, then that seems like it should work.


- Preterm Labor If you are extremely overweight, you have a greater risk of stretch marks (stria atrophica, striae distensae). pregnancy week by week with twins

This works great. Thanks so much!

@Jason, You should be requiring POST for any forms that actually modify data otherwise proxy servers and web accelerators are going to execute your GET requests without the user clicking and it will end up modifying your data. The attacker can't actually read the response from the GET request because of SOP so if the server sends some data it's ok. In other words, GET requests don't need to be protected against CSRF (unless you're modifying data due to GET requests, which you shouldn't be doing).

This works great. Thanks so much!

@Jason, You should be requiring POST for any forms that actually modify data otherwise proxy servers and web accelerators are going to execute your GET requests without the user clicking and it will end up modifying your data. The attacker can't actually read the response from the GET request because of SOP so if the server sends some data it's ok. In other words, GET requests don't need to be protected against CSRF (unless you're modifying data due to GET requests, which you shouldn't be doing).

This works great. Thanks so much!

@Jason, You should be requiring POST for any forms that actually modify data otherwise proxy servers and web accelerators are going to execute your GET requests without the user clicking and it will end up modifying your data. The attacker can't actually read the response from the GET request because of SOP so if the server sends some data it's ok. In other words, GET requests don't need to be protected against CSRF (unless you're modifying data due to GET requests, which you shouldn't be doing).

Do you have a spam issue on this website;
I also am a blogger, and I was wondering your situation; many of us have developed some nice methods and we are looking to swap techniques with other folks, please shoot me an email if interested.

Greetings! Very helpful advice within this article! It’s the little changes that produce the greatest changes. Thanks a lot for sharing!

You've made some really good points there. I checked on the net to find out more about the issue and found most people will go along with your views on this web site.

Have you got any other kind of information connected with this one?
I'd would like to explore more about this unique
area!!!! :-) My partner and I appreciate your current posts, but I might need a little more insight relating
to cruiser customizing. Appreciate it!!!

Post a comment


Type the characters you see in the picture above.