20070802

Adding a Request Token for RemoteField

Link

I've been writing a 15 minute expense tracker in Grails, and it was done in 8 minutes, but I've been spending little snippets of time improving it since that initial 8 minutes. After some of the discussions at BlackHat, I decided I'd try to start making some posts on how to do some things more safely in Grails.
The RemoteField Tag in Grails is uber-easy to use, you give it something like the following:

<g:remoteField name="description"
value="${expense?.description}"
action="updateDescription"
id="${expense?.id}"
update="resultdiv">


The problem is that with all the default documentation, there are two problems with this - key exposure, so you have to check this user's permission to edit the specified object, and XSRF. This isn't a problem with the tag, it's a problem with the easy way of doing things in Grails - and this is identical to Rails. An ordinary CRUD call in [Gr|R]ails makes a URL like http://host/<app>/<controller>/<action>/<id>, and the ID is available as params.id. And that ID is (under normal scaffolding), the primary key of the domain object in question. So an attacker just needs to put on their site an iframe that makes the user do something like http://victim/app/epxense/delete/382, and if there's no permission checking, it goes bye-bye.

Adding a level of indirection to keep the PK in the session and then meaningless ID's on the URL won't solve this because an attacker can just make a user delete all their own entries, etc. So you have to use a token that proves the user visited the page first, and making the user solve a new CAPTCHA for every keyup is silly, right?
So generally, what you see is a hidden form element with a long random token that is also stored on the session. When the form goes back, the two tokens are compared to (somewhat) guarantee that the user was on the correct "setup" page before coming to the submission form.

I started down a handful of paths, and the following were less than elegant:

  • Altering the remoteField itself was less than ideal because you would actually have to add several attributes to the tag to determine how you wanted to deal with the token - do you want to use the same token for all elements on the page? Do you want to use a different token for each element? What do you want to name it? Those decisions have to be carried across to the action that gets called by the remoteField as well.
  • Actually replacing the id attribute with the session token actually worked, but only for one field on one form. If you wanted to do this in several places, because you've abstracted the id out, you'd have to make changes to a lot of existing scaffolding to get it right.
  • One thing I didn't try was keeping the id's from a list() closure in session, keyed by new tokens generated for each one. This would take quite a bit of work, but would also remove some key exposure at the same time - as long as you don't use the tokens in the session to order by. This would also allow you to add token checking as part of a beforeInterceptor because then the keys are the token ID's.
So I ended up with two methods that ultimately worked pretty well.

The first method is to use the paramName attribute of the remoteField tag. If you don't specify the paramName, the value that gets passed back will be with the request attribute "value". So the post parameter looks like "value=The+new+value". When I first put the tag together, I had no compelling reason to use anything other than "value" as the name, so I used this to hold the token:

<remoteField name="description"
value="${expense?.description}"
id="${expense?.id}"
update="statusdiv"
paramName=${token}">


Then in the controller:

if (! params[session.token]) {
render('Bad token')
} else {
...


The second method I tried was to append the token as another request parameter in the id attribute, like so:
id="${expense?.id + '?token=' + token}"

This also works, although it muttles up the tag a bit, but it makes more sense in the controller - just compare session.token to params.token.

Unfortunately, it was still up to me to generate the token, remember to put it in the form fields, and to remember to check it on the way back in. Not perfect, but security always comes at somebody's cost. In this case, it was mine.

3 comments:

  1. This recently came up on Ajaxian.com. and on Joe Walker blog. Joe was saying that the embedding tokens in to urls will be inconvenient for users as they will not be able to bookmark those. Well, I think using a token in the urls (or as request parameters) is the best way to eliminate XSRF and I do not think people will be bookmarking links like delete item, even if they do, with the token based system, the application would take the user to the main page (or some other appropriate page) as the tokens won't match which is not a terrible thing to do.

    ReplyDelete
  2. @kishore - I agree with you - sensitive things (like deletions or address changes) that require a token really don't need to be bookmarked anyway. And here are two really strong reasons the SameReferer header won't work anyway:

    1) SameReferer doesn't stop XSRF through XSS. So if your site has an XSS flaw anywhere on it, I can post back to your CSRF page without having to even calculate a token. (Granted, if there's XSS, you can fetch a new token with a bit more work).
    2) And what about (sadly) actions that are dependent on key exposure? For example, if I don't do permission checking on the submit page, but I do perform permission checking on the preview page where the token is set up? Again, if that's the case, the attacker can do all the work themselves, but XSRF spreads the scapegoats around.

    ReplyDelete
  3. Cool. Really nice points there.

    Your second point is also excellent and I'm sure there are lots of applications that forget to do permission checks at all places and that's why I thought your QA thoughts were spot on and I really liked your other idea of mapping primary key to some session token as well.

    The SameRefererOnly thing will prevent us to do genuine mashups and OEM applications. If mashup applications needs cookies, then the SameRefererOnly cannot be used to do that kind of thing.

    ReplyDelete