Django Rest API Security: Broken Object Level Authorization

Django Rest API Security: Broken Object Level Authorization

OWASP Top 10 API Security Risks – 2023

Introduction

Broken object-level authorization is listed as #1 on the Top 10 API Security Risks – 2023 by OWASP [1]. In this article, we will explore some best practices for ensuring that your Django applications have robust object-level authorization mechanisms in place, safeguarding your APIs against this significant security vulnerability.

[Bonus] We will look at how to prevent data leakages in multi-tenant Django applications.

What is it?

Object-level authorization grants or restricts access to individual objects based on the authenticated user's role or defined permissions. This is useful in applications to control data visibility and prevent accidental data leakages.

Suppose you have an API endpoint /api/object/{id} that retrieves an object based on the provided ID, without proper authorization checks, an attacker can exploit your API in the following ways:

  1. Enumeration attack: The attacker can iterate through different IDs to discover the existence of objects and potentially access sensitive information. By guessing or systematically trying different IDs, they can determine which IDs correspond to valid objects [2].

  2. Insecure direct object references (IDOR): If the IDs used in the API endpoint are predictable or sequential, an attacker can manipulate the ID parameter to access objects that they should not have access to. For example, if objects are assigned incremental IDs, an attacker can simply increment or decrement the ID parameter to access other objects [3].

How To Prevent

Django places a strong emphasis on security and provides several built-in features to protect against various common security threats. These features are designed to help developers build secure web applications with minimal effort.

Do not override the get_object method

It is generally not recommended to override the get_object method unless your endpoint specifically requires fetching data from an external source or you don't need to utilize Django's ORM to build your queryset.

The method provides a consistent way to retrieve objects in your views and is used by the following DRF class-based views:

  • RetrieveAPIView

  • DestroyAPIView

  • UpdateAPIView

def get_object(self):
    queryset = self.filter_queryset(self.get_queryset())

    # Perform the lookup filtering.
    lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

    filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
    obj = get_object_or_404(queryset, **filter_kwargs)

    # May raise a permission denied
    self.check_object_permissions(self.request, obj)

    return obj

Noteworthy

self.check_object_permissions(self.request, object)

With this line, the has_object_permission method of every permission class defined in your view's permission_classes attribute is called. This will raise a PermissionDenied exception if the authenticated user doesn't have sufficient permission for that object.

Good: if you have to override your view's get_object, be sure to call self.check_object_permissions before returning the object.

queryset = self.filter_queryset(self.get_queryset())

This line automatically applies all the filter backends defined in your project's config settings.

REST_FRAMEWORK = {
    ...
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "project.utils.filter_backend.APIBaseBackend",
    ],
   ...
}

This gives you the ability to apply custom filtering logic across all your views globally, ensuring a consistent filtering behavior.

We will see in the next points, how each of these lines makes implementing object-level permissions in our views cleaner.

Custom Permission Class

Create a custom permission class that inherits from permissions.BasePermission or one of its subclasses. This class will define the logic for checking object-level permissions.

from rest_framework import permissions

class UserObjectPermission(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        # Logic to check the object-level permission
        # Return True if the permission is granted, False otherwise
        return obj.user == request.user

The has_object_permission method checks if the object's owner is the same as the requesting user. You can easily implement object-level permissions by adding the new UserObjectPermission to the permission_classes attribute in views that retrieve, update or delete objects.

from rest_framework.generics import RetrieveUpdateDestroyAPIView

class HealthRecordAPIView(RetrieveUpdateDestroyAPIView):
    permission_classes = [UserObjectPermission]
    # ...

With this, an unauthorized attempt to access an object will raise a PermissionDenied error.

Custom QuerySet Filtering

By customizing the get_queryset method in the view, you can apply filters to retrieve specific rows of data based on the access privileges of the authenticated user.

from rest_framework.generics import RetrieveUpdateDestroyAPIView


class HealthRecordAPIView(RetrieveUpdateDestroyAPIView):
    def get_queryset(self):
        user = self.request.user
        return UserHealthRecord.objects.filter(department=user.department)

In multi-tenant applications, where data from multiple tenants or customers is hosted within a shared infrastructure, it becomes crucial to implement rigorous access policies. These policies are vital for preserving data privacy, ensuring security, and maintaining tenant isolation.

One effective way to implement this is by defining a custom filter backend. A filter backend is a component in Django Rest Framework that allows you to modify a view's QuerySet before returning them in the response. It is useful for ensuring that all API endpoints apply a filter consistently across the application. By creating a custom filter backend, you can automatically apply the necessary filters to restrict the data based on the tenant in the request.

class TenantAPIBaseFilterBackend(filters.BaseFilterBackend):
    """
    Filter querysets by tenant
    """

    def filter_queryset(self, request, queryset: QuerySet, view):
        if not isinstance(queryset, QuerySet):
            return queryset

        if hasattr(queryset.model, "tenant"):
            user = request.user
            tenant = user.tenant_id
            queryset = queryset.filter(tenant_id=tenant_id)

        return queryset

You can add this to your project's config:

REST_FRAMEWORK = {
    ...
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "project.utils.filter_backends.TenantAPIBaseFilterBackend",
    ],
   ...
}

This will prevent data leakages within your views by filtering your queries by the tenant in the request.

Unit Test Authorization Requirements

Shift left on security.

When it comes to evaluating the vulnerability of the Authorization requirements of API endpoints, it's crucial to perform thorough testing to ensure that the authorization mechanisms are robust and secure.

An effective approach to ensure the implementation of authorization tests is by

  1. Defining a base class with default failing test cases.

  2. Create a set of skeleton test cases according to your security requirements, user roles and access control policies.

  3. Ensure new API test classes inherit from this base class.

from django.test import TestCase


class BaseAPIAuthorizationTest(TestCase):
    def test_admin_user_request__responds_ok(self):
        self.fail("Authorization test not implemented: Authorized access")

    def test_inactive_admin_user_request__responds_403(self):
          self.fail("Authorization test not implemented: Authorized access")

    def test_non_admin_user_request__responds_403(self):
        self.fail("Authorization test not implemented: Unauthorized access")

For more on permissions, read more from the Django Rest Framework Permission Guide. Django-guardian is a go-to object-level permission backend for most developers. You can check that out as well!

Thanks for reading!

If you enjoyed it, I'm confident that you'll find my other articles equally engaging and informative. Stay up-to-date with the latest content and updates by subscribing to my newsletter.

References