Request Forgeries and ColdFusion - Security Series #9
It's been a while since I posed an entry, and it has been even longer since I posted for my on-going security series. So today I would like to get back to it by looking at Request Forgeries.
What is a Request Forgery?
A request forgery, also sometimes called a Cross-Site (or On-Site) Request Forgery(XSRF), is an attack that is perpetrated against the user of a site who has authenticated access to that site. The user is unwittingly tricked into performing actions on a site through hidden code displayed to them and, therefore, executed in their browser.For a request forgery to work, the user must still have an active, authenticated session for the site that the action will be performed against.
That was confusing as hell, can we have an example?
Yes. Our website (http://www.easilypwnd.com) has a administrators only section for maintaining the website. Bob, one of our administrators is logged into the site and is making routine changes. While he is logged in, he receives an email from Kay in Accounting (but it is actually a spoofed email from 1337 H@x0r MattOverdrive) with a stupid joke. Bob reads the email, chuckles politely at the lame joke and then deletes it, and goes back to administering the website.Everything seems OK until the phone starts ringing. The company website is down.
What happened?
The email Bob received from Kay(MattOverdrive) contained the following image tag:
<img src="http://www.easilypwnd.com/admin/deletePage.cfm?pageid=1" />
Huh, that's a weird image name. It almost looks like a ColdFusion "action page" (1998 called, they want their code back).
When Bob viewed this email, in his email client or web browser, it tried to load the image. So the browser made the request to deletePage.cfm looking for the image, it also passed the parameter for page 1 (probably the site home page). Since Bob is the administrator, and since he was logged in, the page executed the request without issue.
This type of attack could just as easily been carried out if MattOverdrive had submitted the code to a blog comment or forum thread that Bob viewed. The key is that Bob views it while he is logged in to the easilypwnd.com site. And even if Bob wasn't logged into the site, he would not see an error. At most he would see a broken image icon.
So what can a request forgery be used for
Good question. The answer is, just about anything. A request forgery can be used to send almost any type of request to any page on a site. In fact, it doesn't even need to be a public facing page, it could be an intranet page that the hacker knows about. As long as the victim has access to the page, and the request is sent properly, it will be executed. So a site that is vulnerable to request forgeries can be exploited on any action that the administrator (or other authenticated user) can perform. Like deleting users, modifying pages, performing transactions, transferring funds, etc. So, as you can see, this is very dangerous.So what can we do about it?
That's always the big question isn't it. It seems like there is not much we can do. I mean, the browser request is legitimate. The victim site sees a legit request coming from a legit user, with a legit session token, and a legit IP address. All this legitimacy would make us think it is a legit request.The first thought a lot of people might have is to pass some sort of a "secret" parameter only known to the administrator or authenticated user. In some cases, this may work, but that parameter could be compromised, especially if it is static.
Well, what about checking CGI.HTTP_REFERER? If it's not correct, then we know it did not come from the right web form or page, right? That's a good thought, but there are two issues with that. The first is that the HTTP_REFER can be spoofed. The other, and more important, is that in an SSL request, the HTTP_REFER variable is omitted.
OK, how about if we switch to a POST request instead of a GET request. That would stop the request forgery, wouldn't it? Well, actually, yes. It would stop the request forgery example I displayed above, but it would not stop this one:
<form name="rfForm" action="deletePage.cfm" method="post">
<input type="hidden" name="pageid" value="1" />
</form>
<script type="text/javascript">
rfForm.post();
</script>
Well geez, what can we do about it? Will anything work?
A dynamic variable could be used, but how would we pass it and have it verified on the other end? There are actually a couple of ways we can use dynamic, temporal "key" variables to verify that the request came from a legitimate source.
So let's say that we had a dynamically generated form that looks like this:
<form action="deletePage.cfm" method="post">
<input type="hidden" name="pageid" value="#queryPages.pageid#" />
<input type="submit" name="btnSubmit" value="Delete Page #queryPages.pageid#" />
</form>
Well, this form posts to the same vulnerable page that we were exploiting earlier. So how can we secure the action page to remove that vulnerability? Well, we can do this to the form:
<cfset key = CreateUUID() />
<cfset session.formkey.delPageForm = key />
<form action="deletePage.cfm" method="post">
<input type="hidden" name="pageid" value="#queryPages.pageid#" />
<input type="submit" name="btnSubmit" value="Delete Page #queryPages.pageid#" />
<input type="hidden" name="key" value="#Hash(key, "SHA-256")#" />
</form>
And then in the action page you would have the follow code BEFORE you do any processing:
<cfif StructKeyExists(form, "key") AND form.key EQ Hash(session.formkey.delPageForm, "SHA-256")>
<cfset structDelete(session.formkey, "delPageForm") />
<cfelse>
<cflocation url="/" />
</cfif>
So this code creates a UUID and saves that UUID to the session scope under a key that is unique to this form. It then passes a hash of that key to the processing page. The processing page receives that hash and compares it to a hash of the key stored in the session variable, if they match, the key is deleted and the processing is allowed to continue, if they do not match, then the request is invalid and the user is redirected.
The complexity of this can be increased as you, the developer, sees fit. If you want the key to be a combination of the userid, jsessionid, a UUID, and the last 4 of the users SSN, then go for it. Just make sure you hash it and then rehash it on the other end. The passed hash value can be any combination of unique identifiers, as long as it can be reconstructed on the other end and as long as it is not constructed entirely of values that would be available to the hacker. For example, a bad hash would be comprised of the current date and the username.
I'm sure I missed something about Request Forgeries in this post, but at the moment, I cannot think of what. I'm sure that the method outlined above is not the only way to protect against request forgeries, so if you know of another way, please add it to the comments.



Of course, as you say, with a "Static" value, the likelihood of compromise goes up. And, I don't want to misquote Jim Harris.
All in all, I like what you are doing here. I used to be opposed to anything that tied SESSION or COOKIE to a given screen, but I think something like this can be easily done without impacting the user experience too much (if they use multiple tabs, etc.).
using a cookie is not very safe, because if the user - who must be logged on in the target application for the XSRF to work - happens to already have received a valid cookie for the request that the attacker wants to perform, that cookie will be automatically submitted by the browser even if the request has been originated from the attacker's site, so you'll believe the request is legit.
Andrea
If an application has a unique, fixed reference for a given request (e.g. a form), should the attacker be able to create a valid account that gives him access to the target form (with his own account) he'll be able to read the token and copy it into his forged request. From there on, he can have any user that visits his site unwittingly perform the action on his own behalf. Certain actions might be out of the attacker's reach because he doesn't have access to the form at all unless he is an admin, but all the common user actions would be vulnerable.
The whole protection resides in the fact that the token is impossible to know by the attacker - without hijacking the session, in which case he wouldn't need to resort to a XSRF at all.
Producing a random token with CreateUUID, storing it in the session and adding it to the form as a hidden field achieves just this, because UUIDs are rather impossible to guess, and only the person logged in with that particular session will perform requests with that particular token.
With this in mind, also hashing the token is not essential: what is important is the fact that the token is always only included in requests prepared by the real site for the legitimate user.
Because in many cases they are not prepared in two steps (first set up a form, then process the submission), you will not be able to set up an UUID in the first and expect to find it returned by the second.
Allthough we all know that GET requests should not be used to alter any data, there are many cases in which this is done nevertheless, and plenty of smooth Ajax transactions are carried on using GETS.
Replace all such GETs with POSTs if you can - and if you can't, you'll have to create one, random session-wide token at login time, store it in the session as usual, and accept *any* GET request that carries this particular token in one form or the other (and which the current user is authorised to request, of course). You *may* 'consume' this token and replace it with a new one for any subsequent request, but this would raise the risk of false positive alerts, probably to unacceptable levels: as soon as the user opens an internal link in a new tab or window, the old tab/window is almost sure to loose her legitimacy because she is still holding the original token.
Note that this same risk is associated with creating a new UUID each time a form is requested: and this is the reason why in fact I don't: I create a single token for the whole session at login time, and any request made by that user with that token (in an hidden field for POSTs or in some other part of the request for GETs), is accepted until he logs out or the session times out. (I believe, however, but still need to work on it, that this practice has disadvantages because if I also have an XSS flaw I increase the risk of the token being exposed.).
The final last thing to consider is how you can make your GET request use the token. We've already seen that cookies are not an option. In all my Ajax requests, I add a custom HTTP header to the request, to reduce the visibility of the token. In the others the only possibility I can see is in the URL. I'd be very interested in reading any ideas for an alternative solution.
Thanks for the comments. Ajax security is something I need to explore more indepthly before I comment on it. It is on my list of things to learn more about. One of the things I want to explore is security implementation differences between making Ajax requests directly to the service layer versus making them through the controller(Mach-II, MG, ColdBox, etc).
It's hopeless.
You make good points. I hadn't thought about it that way, but now I see what you are saying about the dangers of static form references. I could very well be wrong about what Jim was saying (he seemed to know his stuff!).
Just an FYI - tried to use this w/ a little copy and paste and was rudely scolded by CF
<cfif StructKeyExists(form, "key") AND form.key = Hash(session.formkey.delPageForm, "SHA-256")> is incorrect :)
it should be form.key EQ not form.key =
I am combining your salting and hashing techniques together with this form submission technique. I am finding an anomaly.
First off I have the form set up as you state above:
<cfset key = CreateUUID() />
<cfset session.formkey.delPageForm = key />
<input type="hidden" name="key" value="#Hash(key, "SHA-256")#" />
For the sake of being able to test I set the field to text instead of hidden so I can see it's value.
When I load the form I get the following values:
session key: 245EECFD-AA19-34A6-0CDB3E4C36E11751
form field called key: 2B950389DA513956E72C3DF0B7AE27C081A24C1806FAB7060969DC66AD86490D
Then I submit the form deliberately placing a two distinct passwords in my password confirmation set. I get an error stating: The passwords you entered do not match This is exactly what I expected.
Now I look at my form again and the form.key is still equal to 2B950389DA513956E72C3DF0B7AE27C081A24C1806FAB7060969DC66AD86490D
However the session key is now 24620DF4-F1DF-076E-558C47D67875FE97
So the next time I submit my form - no processing will ever happen.
It took me a while but I finally realized that the wonderful little setting preservedata="yes" was on and so my form.key never ever changed.
I just thought I would bring this up for anyone who tries this in the future - it works great - but in order to persist your data - you will have to cfparam all the form variables yourself EXCEPT the form.key :)
I have not tried this, but perhaps there is a way to do it. Cause losing preservedata when you are otherwise able to use it, would suck.
<cfset key = CreateUUID() />
<cfparam name="session.formkey.newBusiness" default="#key#" />
<cfinput type="hidden" name="key" value="#Hash(key, "SHA-256")#">
Then on the other end it is the same as it already was ...
<cfif StructKeyExists(form, "key") AND form.key EQ Hash(session.formkey.newBusiness, "SHA-256")>
So there you have it - if you want to use the preservedata="yes" you certainly need to param your session.formkey variable. However - remember to erase it once you have put it to use :)
Thanks for the oh so simple solution.
Chris
1. Your param could have CreateUUID() in the default=""
2. I never added it to this post, but I have since realized that in the processing code, we should also be doing a structKeyExists() on session.formkey.newBusiness before we compare it to form.key
Thanks for looking into that Chris, very helpful.
<cfset key = CreateUUID() />
<cfset session.formkey.delPageForm = key />
But hey -why not save some typing huh :) I like it.
I like the idea of the structkeyexists() The only trouble I see is that session.formkey is not defined when we do the test at the top of the page - unless we create it in the onSessionStart() function. I think you may have mentioned that in another post. But if someone doesn't want to define it there and have it hanging out - they can just do a simple isDefined('session.formkey.delPageForm') if they want.
I think I like the idea of setting in the onSessionStart and then dealing with structkeyexists() - this allows us to more easily document the most common features we are using. Now I'm just babbling so - thanks and have a great night!
Chris
You are missing an output tag around the "key" form field, but then it works nicely.
This helps me with a noob flash application I am fighting with. I have a small flash game of a slot machine embedded in a CF page in a directory secured by application.cfc and session variables enabled. The only way I have found to pass back data from Flash to CF is with
dataOut = new LoadVars();
dataOut.credits = credits;
//Using send ( ); method to send the data
dataOut.send("http://mywebsite.com/secure/slots.cfm", "_self", "POST");
This works fine but the variables are presented back to CF as a form.var . I think I can use your technique to pass a hashed key into my swf when it loads and back out to my CF page when the user is "cashing out", which will stop the form handler part of my CF page being abused.
<input type="hidden" name="key" value="<cfoutput>#Hash(key, "SHA-256")#</cfoutput>" />