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.