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

Comments
Ben Nadel's Gravatar Last month at the NYCFUG, Jim Harris came to give a presentation on securing web sites. One of the things he discussed was very similar to this approach. But, rather than move things into the session, I believe he used a COOKIE value to store the "referring form" key. I can't remember off-hand how the key was passed, but I think he had a unique reference for every form in his application (rather than using a UUID).

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.).
# Posted By Ben Nadel | 8/27/08 1:55 PM
Andrea's Gravatar Ben,

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
# Posted By Andrea | 8/27/08 5:22 PM
Andrea's Gravatar @Ben again... probably I'm not understanding what you mean with a static unique reference for every form in the application, but maybe it is worth clarifying one aspect of the XSRF.

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.
# Posted By Andrea | 8/27/08 5:57 PM
Andrea's Gravatar A last consideration: there is something to be said about GET requests, too.

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.
# Posted By Andrea | 8/27/08 6:28 PM
Jason Dean's Gravatar @Ben - Thanks for the comment. I understand what you are saying about tying forms or specific pages to the session or cookie scopes. I don't know if it would make it any smoother, but this might be a place to consider using a session facade. That way , if you decide to change your implementation, you don't need to change the API for the facade. Just a thought. I am no expert on facades, but I think this is a case where it might be useful.
# Posted By Jason Dean | 8/27/08 8:42 PM
Jason Dean's Gravatar @Andrea - I am going to start charging you per word ;) I think my bandwidth bill is going up.

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).
# Posted By Jason Dean | 8/27/08 8:47 PM
Andrea's Gravatar I know! Sorry for that. I do read my comments before posting them, and usually I manage to shorten them by a sentence or two. But while doing so I also realise that I was forgetting something that ought to be said...

It's hopeless.
# Posted By Andrea | 8/28/08 2:33 AM
Ben Nadel's Gravatar @Andrea,

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!).
# Posted By Ben Nadel | 8/28/08 7:37 AM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner
Clicky Web Analytics