Keep your hands off my tastypie

I’ve been using tastypie, the very awesome django REST API framework for a little while now (btw, that’s not the official title, but it might as well be). I’m not going to write yet another comparison between tastypie and django-piston. My reasons for choosing tastypie were that its code looked nicer, and it seemed a much more active project.


One of the things that I immediately liked about tastypie, being a security-geek and all, was the security framework built into it. Primarily the authentication and authorization classes. They make it very easy to extend, and almost a no-brainer to apply to any resource. This means that providing resource-level authorization is also very easy and clean.


However, whilst working with tastypie and applying some authorization rules to my resources, I noticed a couple of pitfalls. Those are quite easy to miss if you’re not very familiar with the tastypie codebase. I wouldn’t say it’s a vulnerability or a bug as such, perhaps more of a (sub-optimal) design choice from a security-perspective. That said, if you use tastypie incorrectly, or unaware of those pitfalls, you might create a security vulnerability on your otherwise delicious API.

It’s possible or even likely that some of those things will improve in tastypie, and there’s already some discussion going on. But until then – it’s no excuse to leave your authorization rules open or not use them to the fullest! You can apply a few common patterns to your code to use tastypie even more securely than it already is. Or at least avoid a false sense of security, if you’re applying it without being aware of those pitfalls.

Overrides

Some times it makes sense to ignore and override certain values with a constant or a pre-defined per-user value. For example consider this resource

class BlogPostResource(ModelResource):
    author = fields.ToOneField(UserResource, 'author')

    class Meta:
        queryset = BlogPost.objects.all()

We have a blog post resource, and each blog post is linked to an author. This will work great with GET requests for blog posts and will display the linked author correctly. However, when updating (PUT) or creating (POST) a new blog post, we want to make sure the correct author is linked to the blog post. We usually don’t want to (and shouldn’t!) rely on the user to provide the correct value. The easiest thing is therefore to override any supplied author in the request, with our request.user object.

So a simple solution would look something like this:

class BlogPostResource(ModelResource):
    author = fields.ToOneField(UserResource, 'author')

    class Meta:
        queryset = BlogPost.objects.all()

    def obj_create(self, bundle, request, **kwargs):
        return super(BlogPostResource, self).obj_create(bundle, request, author=request.user)

Makes sense. Right? We’re supplying obj_create with an explicit value for the author from request.user. Wrong! Well, not completely wrong. This code will work, and will provide the current user to obj_create. However, if the (malicious) user provides an alternative value e.g.

"author" : {"id": "2"}

then this value will take precedence over the kwargs you provide in your code. That’s very easy to miss, and can potentially have undesired consequences.


A quick-fix for this would be to make sure you override any values inside the bundle, and don’t rely on kwargs.

    def obj_create(self, bundle, request, **kwargs):
        bundle.data['author'] = {'pk': request.user.pk}
        return super(BlogPostResource, self).obj_create(bundle, request, **kwargs)

[UPDATE]
An alternative method, which might actually be preferable, more elegant and less confusing, is using the hydrate_author method instead.

apply_limits

apply_limits is the function which allows you to filter out certain data, based on authorization decisions. The examples on the tastypie documentation are quite clear, so I won’t repeat those. I find it very useful for applying fine-grained authorization decisions, and perhaps even easier and more effective than the is_authorized function on the same class. Why? apply_limits not only applies a binary decision (yes/no). It allows you to filter out unauthorized values instead of blocking the whole request. This works fantastically well for GET and DELETE requests. It only gives back those records that are valid and filters out anything else. So you won’t be able to, e.g. delete other people’s records, or view data which you are not the owner.


When it comes to PUT/POST requests however, it is currently not applied in the most optimal fashion. One of the biggest pitfalls is not realising that apply_limits is not used at all when POSTing new data. It’s simply not a part of the obj_create code. If you know enough about django, then it makes some sense. When creating a new object, you’re not using the queryset filter method. You’re just instantiating a new model object, then saving it. That doesn’t mean that you necessarily want to allow creating new objects, and even if you do allow it, you might want to enforce certain limits on an attribute-level. That is, column-level authorization or validation.


This can be applied using data validation, but if you want to override data (as I tried to illustrate above), and also use the request object in either the decision process or as a data source, validation is not the best way. apply_limits does seem to me like the correct function to use. So how can we use the apply_limits for POSTs ? Here’s a simple example, extending the code I used above:

class BlogPostAuthorization(Authorization):

    def apply_limits(self, request, object_list=None):
        if request and request.method in ('GET', 'DELETE'):  # 1.
            return object_list.filter(author=request.user)

        if isinstance(object_list, Bundle):  # 2.
            bundle = object_list # for clarity, lets call it a bundle
            bundle.data['author'] = {'pk': request.user.pk}  # 3.
            return bundle

        return []  # 4.

class BlogPostResource(ModelResource):
    author = fields.ToOneField(UserResource, 'author')

    class Meta:
        queryset = BlogPost.objects.all()
        authorization = BlogPostAuthorization()
        allowed_methods = ['get', 'post', 'put', 'delete']

    def obj_create(self, bundle, request, **kwargs):  # 5.
        bundle = self._meta.authorization.apply_limits(request, bundle)
        return super(BlogPostResource, self).obj_create(bundle, request, **kwargs)

    def obj_update(self, bundle, request, **kwargs):  # 6.
        bundle = self._meta.authorization.apply_limits(request, bundle)
        return super(BlogPostResource, self).obj_update(bundle, request, **kwargs)

What’s happening here?


First of all, I moved the code that overrides the author into the apply_limits call (#3). This seems the more logical spot to apply authorization overrides. It comes at the ‘cost’ of having a specific authorization class per resource. I believe this is the correct approach however. If we apply authorization at this fine-grained level, we would need a dedicated place for it.


Notice anything else?


#1: Checking if the request is GET or DELETE. Seems like a strange duo, but both operate in a similar way by applying the django ORM filter.


#2: Checking whether or not this is a bundle. This is because we’re passing a bundle instead of the typical QuerySet to apply_limits.


#4: Returning an empty list. From a security standpoint it’s always best to fail-safe / fail-close.


#5, #6: Using apply_limits consistently with both obj_create and obj_update using the bundle data.


Compare this with the following code snippet, which I came across recently. On first glance it seems sensible, but notice that if the request is missing, user is not present or the request is not GET, it simply returns the object_list without any filtering. Bad idea.

if request and hasattr(request.user) and request.method == 'GET':
   return object_list.filter(author=request.user)
return object_list

[UPDATE]
A slightly different approach could be to override the apply_authorization_limits inside the resource itself. This function typically calls the apply_limits of the authorization class, but there’s no reason not to use it directly on the resource.

is_authorized

The is_authorized function is the second (or first, depends on your perspective) of the two core functions inside the Authorization class. Tastypie uses it by default before it grants access to any request. This is a great first-barrier to block unwanted requests early on. If you can, I recommend using (or extending) the DjangoAuthorization class, which fits in nicely with any existing permissions on your django instance. It produces a logical mapping between POST, PUT, DELETE and add_, change_, delete_ django permissions (respectively). Note however that GET is always allowed. If you have a need to block read access from certain users or groups to certain resources or under certain conditions, you’d have to implement this yourself.

Explicit allow

Tastypie also supports the PATCH method. However, since it is not very commonly used, you’re likely to ignore it when writing your authorization code. In fact, it was also left out from Tastypie’s own DjangoAuthorization I just mentioned. PATCH is however included by default, and if you sub-class any authorization other than the default (and secure) ReadOnlyAuthorization, you might open up yourself more than you’d expect. The easiest thing is to make sure you explicitly specify your allowed methods. If you don’t plan to support a method on your API, simply don’t allow it. If you do allow it, make sure you check for it in your own authorization. The fail-close defaults I mentioned, together with making sure you use apply_limits on obj_update should keep you safe (please note I haven’t tested this myself, since I haven’t had the need for the PATCH method yet).

Final tip

Just a quick final tip: Tastypie seems to return a 401 when a request is not authorized. It should probably return a 403 Forbidden response, which is more suitable if the request is authenticated, but not authorized. If you want, you can make tastypie return a 403. This is already covered on github, So I won’t post it again. One thing to note however: if you do call is_authorized somewhere in your own code, don’t always expect a True/False response. In particular, don’t do something like this:

if self._meta.authorization.is_authorized(request):
    # do something...

If is_authorized returns an HttpResponse object (even with status code of 403), then the above condition will evaluate as True. Not what you want.

Quick summary

Tastypie is a great framework, very robust and very easy to get started. Some times however, this simplicity means some aspects are hidden away. If you don’t have time to learn the framework well, then you’re bound to make some assumptions. Sadly, wrong assumptions can lead to unexpected behaviour. Hope I managed to illustrate the pitfalls and workarounds clearly and help you avoid some blind-spots when it comes to resource-level authorization using Tastypie.


[UPDATE]
I’ve received some feedback from Daniel Lindsley, the author of tastypie, who was kind enough to review this post and make some suggestions and corrections. I’ve updated the post and tried to highlight those comments.

2 Responses to “Keep your hands off my tastypie”

  1. Nico

    Thanks for a great article. I found it while looking to overcome the problem of users self-registering on a site (as anonymous) but no allowing any updates as anonymous. This article has shed some light on how to continue.

  2. Tomasz Zielinski

    Great article! There’s one more thing to watch out for – `apply_limits()` seems to be called also for related object, but then its `request` parameter is set to the original request.

    Example: if you update a blog entry which has comments, then `apply_limits()` called for those comments will have `request.method == ‘PUT’`, which means that if you rely on `request.method` inside `apply_limits()` you might end up being confused.

Leave a Reply

© 2012 Gingerlime