Keep your hands off my tastypie

Update

Please note that since Tastypie v0.9.12 the authorization framework was rewritten. Lots of information on this post no longer applies. I’m hoping to write a follow-up post at some stage.

Original Post

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 (for more info, check out the documentation of the hydrate cycle).

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

prepend_urls

Special thanks to Nick for pointing out this pitfall, which certainly deserves mentioning. I’ve never used prepend_urls, so wasn’t aware of this issue.

prepend_urls allows you configure different urls for resources, similarly to how you would normally link a url to a view in urls.py. If you are not careful, the view being called is not authenticated or authorized for you by tastypie. You would either have to use one of the methods that apply authentication/authorization, such as dispatch, dispatch_list, or add authentication and authorization checks inside your views. Be careful of calling methods like get_detail, get_list which don’t apply authentication and authorization.

Some examples in the tastypie cookbook wrap dispatch or dispatch_list and are ok. Others implement their own views. One example does check for authentication (but not Authorization!). However, as Nick pointed out, I also found another example where it doesn’t look like authentication was being checked and might not be safe.

Given the nature of prepend_urls being on the url/routing level, if you know django and understand MVC architectures, you should probably expect this. However I think Nick is absolutely right that this should be highlighted as a potential pitfall. If someone implements this expecting prepend_urls to take care of authentication and authorization out-of-the-box — it won’t.

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.

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

  3. zVictor

    Thanks! It helped me a lot, and was exactly what I was looking for.
    However, I was having some problems with this approach.
    In my situation, when I do a PUT, I don’t understand why, it runs a final `apply_authorization_limits` not with a Bundle, but with a QuerySet. So, there is my code:

        def apply_authorization_limits(self, request, object_list=None):
            if isinstance(object_list, QuerySet):
                return object_list.filter(proprietario=request.user.pk)
     
            if isinstance(object_list, Bundle):
                bundle = object_list
                bundle.data['proprietario'] = {'pk': request.user.pk}
                return bundle
     
            return []
    

    Whenever it has a QuerySet in hands, it will be filtered, and if it has Bundles, forces it. Maybe it would be better to raise an exception instead of return empty, I am not sure yet.

  4. Yoav Aner

    Hi Victor,

    Are you overriding `obj_update` similarly to the example I provided (marked as #6 on the post)?

    Unfortunately I’m not using tastypie very much these days, and I’m aware of some changes being made in tastypie on that front, so it’s also possible some or most of those tips are no longer necessary, or that there’s a more standard way to accomplish the same thing…

  5. Yoav Aner

    Hi Tomasz,

    Sorry I missed your comment earlier. You made a very valid point and I am aware of it. The examples on this post were just a starting point really… Of course you need to be aware of how tastypie handles related objects to take care of all conditions.

    Related objects really make things tricky, also security-wise. I’ve discussed some concerns there with Daniel Lindsley actually, but I’m not fully sure where things are standing currently.

  6. JohnRandom

    I recently found another security problem in the resource module. When you update a resource, all related records are saved with save_related() (see obj_update on the ModelResource). But: there is no further authorization check upon those related records. In some setups a malicious user could use a resource he is authorized to write to sending a related record, which would be the real target.

    The only reasonable way I found to circumvent this problem was, to deactivate the implicit update-by-post by raising an HttpNotFound.

  7. JohnRandom

    Oh, I just saw that you were already aware of this problem. Sorry, I did not read through all of the commentary…

  8. zvictor

    Yeah, I did it. I will show you more of the code. As you can see, I didn’t change Authorization, ’cause apply_authorization_limits do the entire dirty work by itself.

        def apply_authorization_limits(self, request, object_list=None):
            if isinstance(object_list, QuerySet):
                return object_list.filter(author=request.user.pk)
     
            if isinstance(object_list, Bundle):
                bundle = object_list
                bundle.data['author'] = {'pk': request.user.pk}
                return bundle
     
            return []
            
        def obj_create(self, bundle, request, **kwargs):
            if not request.method == "POST":
                raise BadRequest('Object not found or not allowed to create a new one.')
            bundle = self.apply_authorization_limits(request, bundle)
            return super(EstabelecimentoResource, self).obj_create(bundle, request, **kwargs)
                
        def obj_update(self, bundle, request, **kwargs):
            if not request.method == "PUT":
                raise BadRequest('Object not found or not allowed to update.')
            bundle = self.apply_authorization_limits(request, bundle)
            return super(EstabelecimentoResource, self).obj_update(bundle, request, **kwargs)
     
        class Meta:
            authorization = Authorization()
    

    As you can see, I changed the original behavior of create and update, not allowing to create new instances if not explicit asked to do that

  9. Federico

    Im new on python,django and tastyspie

    When i tried to use this code as base for my development when i do a post to create a new entry on the DB the response its “global name ‘Bundle’ is not defined”.
    For the line : if isinstance(object_list, Bundle):

    so.. there is something that i missing or the example it is outdated?

  10. Yoav Aner

    Hi Federico,

    Sounds like you’re simply missing an import. These examples are not complete python files/modules. Try adding `from tastypie.bundle import Bundle` and this should probably help.

  11. KevinZhao

    Hi,
    when I use obj_create method, server returns this error:
    “error_message”: “The format indicated ‘multipart/form-data’ had no available deserialization method. Please check your “formats“ and “content_types“ on your Serializer.”

    I don’t know what happened. Could you help me?

    Here is the code:

    class RegisterResource(Resource):
        class Meta:
            queryset = Customer.objects.all()
            resource_name = 'register_customer'
            authorization = DjangoAuthorization()
            serializer = Serializer()
    #        authentication = BasicAuthentication()
            
        def obj_create(self,bundle,request,**kwargs):
            print bundle.data['firstname']
            print request
            print kwargs
            bundle = super(RegisterResource,self).obj_create(bundle,request,**kwargs)
            return bundle
    

    request_url:
    http://localhost:8000/api/v1/register_customer/

    the data in form:
    firstname=Xin

  12. ephess

    Good article, helped me with a few issues I’ve been working through. Thanks for taking the time to write it.

    You mentioned in an update:

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

    Are you able to provide some further information on this (a link to some documentation or the section of the code base implementing this perhaps?) – this sounds like exactly what I’m looking for however I’m unable to find any reference to this in the documentation.

    Thanks!

  13. Yoav Aner

    Thanks for the feedback. As for hydrate_author, have a look at the the hydrate cycle on the tastypie docs. Essentially, you can use either the hydrate hydrate_FOO methods, the latter uses a fieldname (e.g. hydrate_author). It allows you to manipulate the bundle.obj or the bundle.data and overwrite/change the data provided for your fields.

  14. Nick

    I think top of your list should be:
    “Methods invoked via prepend_urls do not have ANY auth applied to them – you need to either do this explicitly yourself, or use dispatch if you want the resource’s auth applied”.
    Big pitfall. Indeed there are examples of using prepend_urls in the cookbook with no mention of this.

  15. Yoav Aner

    Thanks Nick. I’ve never used prepend_urls, so wasn’t aware of this potential pitfall. I’ll update my post to mention this.

  16. Craig

    It’s great to read stuff like this about how modules work under the hood. Great write up!

  17. Yoav Aner

    Thanks Craig. Please note that some of this info no longer applies since the changes to tastypie in 0.9.12. I would have to update the post or write a follow-up at some point…

Leave a Reply

css.php