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
.
How to fix it ?
Fixing an IDOR vulnerability requires two things:
- Hiding the id of the user
- 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})
Second better solution (recommended)
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 :
- By email at [email protected].
- If you want to know more about me, you can check out my about page.
You can use my articles with no cost or attribution, feel free to edit them and make them your own.