Django is by design very secure, most of the commons vulnerabilies such as XSS, CSRF, SQL injection, clickjacking are fixed by the simple use of the framework, but there is one that is not fixed by default, and it is the Idor vulnerability.

In this article we will see what it is and how to fix it.

What is an Idor vulnerability ?

Idor is a vulnerability that occurs when a user can access a resource that he is not authorized to access. This vulnerability is common in web applications that use a sequential number such as a primary key in the URL to identify a resource and Django is no exception.

But as always the security flaw is usually introduce by us, the developers, and one of the most common security flaw is the Idor vulnerability.

Broken Access Control is the first vulnerability in the OWASP Top 10, and Idor is a subcategory of this.

Example

Let’s say you are designing your user model and you want to give the user the ability to change his password, you will probably create a view that will look like this:

def change_password(request, user_id):
    user = User.objects.get(id=user_id)
    if request.method == "POST":
        form = ChangePasswordForm(request.POST)
        if form.is_valid():
            user.set_password(form.cleaned_data["password"])
            user.save()
            return redirect("home")
    else:
        form = ChangePasswordForm()
    return render(request, "change_password.html", {"form": form})

And you will probably create a url that will look like this:

path("change_password/<int:user_id>", views.change_password, name="change_password")

This is a very common way to do it, the primary key is used to identify the user for which to change the password, and since Django use a sequential number to identify the user, it is very easy to guess the id of another user and change his password.

For example, if the user with the id 1 is the admin, then the user with the id 2.

idor

How to fix it ?

Fixing an IDOR vulnerability requires two things:

  1. Hiding the id of the user
  2. Checking if the user is authorized to access the resource

Hiding the id of the user

The first fix and the easiest one is to use a random number to identify the user, fortunately Django has a built-in class to generate random chars, it is the UUIDField class, so we can change the id field in the User model to be a UUIDField:

import uuid
from django.db import models


class UserModel(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    # ...

Make sure to set a default value for the id field since the database will not generate a value for it, also primary key must be unique, so we set the primary_key to True.

Caveat

Now this is good, but hiding the id is not enough, this is call security through obscurity and while the id is hard to guess and could help to prevent the Idor vulnerability, it is not enough, we need to check if the user is authorized to access the resource.

Checking if the user is authorized to access the resource

By default we should apply the principle of least privilege (POLP), which means that the user should only have access to the resources that he is authorized to access, so we need to check if the user is authorized to access the resource.

In Django, we can use the request.user to get the current user, so we can change the change_password view to look like this:

First solution

def change_password(request, user_id):
    user = User.objects.get(id=user_id)
    if request.user != user:
        raise PermissionDenied
    if request.method == "POST":
        form = ChangePasswordForm(request.POST)
        if form.is_valid():
            user.set_password(form.cleaned_data["password"])
            user.save()
            return redirect("home")
    else:
        form = ChangePasswordForm()
    return render(request, "change_password.html", {"form": form})

Using the ID is ok, but URL can be modify and since Django come with a protection against CSRF, we could use a post request to change the password, so we can change the change_password view to look like this:

def change_password(request):
    if request.method == "POST":
        form = ChangePasswordForm(request.POST)
        if form.is_valid():
            user = request.user # get the current user from the request, no need to use the id
            user.set_password(form.cleaned_data["password"])
            user.save()
            return redirect("home")
    else:
        form = ChangePasswordForm()
    return render(request, "change_password.html", {"form": form})

And the url will look like this:

path("change_password", views.change_password, name="change_password")

Conclusion

IDOR is a very common vulnerability, and one that can be easily underestimated, for example if an user can access private data of another user, it can be a big problem, even worse, allowing an user to change the password of another user can be a disaster especially if the user is an admin.

Ressources


I hope this article was helpful, if you have any question or suggestion, feel free to reach out to me on :

You can use my articles with no cost or attribution, feel free to edit them and make them your own.