Photo by Kyle Glenn on Unsplash
Django Broken Access Control Guide
The security of web applications is paramount for every business. Users want to know that their data is secure and private.
Data is King. Access to more data is akin to holding the master key to a kingdom.
~ The contemporary Laptop and high society Lady
Introduction
In this post, we'll go over what broken access control is, offer some examples of it, and look at some of the ways we can improve the security of our Django applications. Hopefully, you'll learn something useful that you can apply to your Django projects.
Broken Access Control
First up, we'll define broken access control and look at some of the forms it takes. Broken access control describes the exploitation of access control management by attackers and bad actors. The security of web applications is paramount for every business. Users want to know that their data is secure and private. With this in mind, we should design web applications for privacy, integrity, and data security.
Types of Broken Access Control
Some known vulnerabilities of broken access control include the following:
- Manual app state modification. These modifications could be URL modification, browser cookies and sessions, or the use of custom API attack tools.
- Key identifier change. This allows the alteration of key identifiers, like the user’s primary key, in such a way that gives unwanted access to another user to perform actions otherwise unauthorized.
- Privilege escalation. This is a known method of attack where an attacker logs into a business database as an administrator. This attack can take the form of acting as an authenticated user without authentication.
- Metadata manipulation. This form of exploitation involves modifying or invalidating an application's metadata, such as the JSON Web Token.
- CORS misconfiguration. This allows an unauthorized API to access business resources.
Without a doubt, access control exploitation impacts businesses negatively. It can lead to the disclosure of sensitive business data or the modification or destruction of business data, as well as allowing unauthorized users to perform actions they should not have access to or the right to perform.
As a result, different languages and frameworks deal with these five known access control flaws in different ways. It's critical for developers to understand what access control is and how to manage access to application resources.
Fortunately, Django has made it possible to mitigate these security vulnerabilities. Next up, we'll look at several examples of faulty access control and how to avoid them in the sections that follow.
Manipulating Parameters to Alter Results
Assume we have a URL like https://example.com/accounts/details?id=123&access_key=abcdefg&access_secret=opensecret
. An attacker may change the URL parameters such as the ID, ACCESS KEY, and ACCESS SECRET to anything malicious, giving them access to account information.
Validation and verification of requests should be in place to avoid such a scenario. For example, verify the user attempting to perform such actions as well as validating the request data and the requesting user's access credentials.
def update_details(request, acc_id):
user = Account.objects.get(acc=acc_id)
if request.user.id == user.id:
# ALLOW ACTION
# VALIDATE REQUEST DATA
form = AccountForm(instance=user, request=request)
...
else:
# DENY ACTION
Also, it's a good idea to use more difficult-to-enumerate references. Usually, randomly generated strings are recommended as reference identifiers rather than predictable auto-incremental integers. With this in mind, the user needs authorization for requesting data before the server provides it.
Changes to the Key Identifiers
Let's say we have a URL like https://example.com
, and regular users can only sign up, log in, and add and update their own biodata. Assume you can get the user biodata at https://example.com/accounts/profile/<ID>/
by replacing the ID. An attacker can access this route by manually entering it into the browser because this endpoint is not accessible to any user as a button.
Assume the attacker inputs the address manually in the example above: https://example.com/accounts/profile/2/
. If the attacker succeeds in modifying User 2 data regardless of not being authenticated as User 2, the attacker has acquired undesired authorization to perform actions as User 2.
In order to deal with this situation, we need role-based permissions and object-level permissions so that authorization can be verified between the authorized user and the requested object resource.
from django.db import models
class UserProfile(models.Model):
user = models.OneToOneField("auth.User", on_delete=models.CASCADE, related_name="profiles")
address = models.TextField(blank=True, null=True)
profession = models.CharField(max_length=30, blank=True, null=True)
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.user == request.user
class UserProfile(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsOwner]
In this case, we imposed data ownership on the Profile object to prohibit others from obtaining the profile data. The profile resource object is returned if both the requesting user and the profile owner are authenticated; otherwise, the page returns a 403 HTTPS status code. Third-party packages like django-guardian and django-rules are designed to address views and object-level authorization.
CORS Misconfiguration
Cross-origin resource sharing (CORS) is an HTTP-based mechanism that lets a server specify domains, ports, or schemes from which a browser can obtain resources. For example, if the CORS configuration on our Django application is set to True
for all requests from example.com, our Django application will be vulnerable. Because alternative HTTP methods are allowed, an attacker may use https://example.com.chicken.wings
, which is acceptable because CORS permits origin headers with example.com. An attacker might use this vulnerability to impersonate a user and get access to sensitive information.
To rectify this situation, hence, we probably shouldn't set the following:
CORS_ORIGIN_ALLOW_ALL = True
Rather, we generally should deny all CORS origins and provide an allowlist of CORS origins:
CORS_ORIGIN_ALLOW_ALL = False
CORS_ORIGIN_WHITELIST = (
"https://example.com",
)
A Django third-party package like django-cors-headers is recommended for CORS configuration.
Metadata Manipulation
Consider a case in which we place our Django application's source code in the server's public file directory. Perhaps we left some metadata exposed in our code, such as remote addresses or information from third-party cloud providers. An attacker can gain access to our application by altering this sensitive metadata.
With this intention, one of the ways to mitigate this is to avoid exposing sensitive data by setting the DEBUG
option in our settings.py file to False
.
DEBUG=False
Also, we should use environment variables rather than hardcoding sensitive data. Third-party packages such as django-decouple and django-dotenv are designed for this specific purpose.
CLOUD_ACCESS_KEY = xxxxxxxxxxxx # use environment variables
CLOUD_ACCESS_KEY = "this-is-a-hard-coded-value" # Do not hard code values like this
Subsequently, when sending error logs via emails to our Django application admins, we should use the Django sensitive_variables()
decorator to hide data that may be sensitive. Let's see an example:
from django.views.decorators.debug import sensitive_variables
@sensitive_variables("storage_name","region")
def notify_admins(cloud_provider):
storage_name = cloud_provider.storage_name
region = cloud_provider.region
...
Also, we should prevent our server hosting the Django application from listing directories or paths in production. This is to prevent attackers from traversing directories for unauthorized file access.
Rate Limitation and Access Checks
One of the ways attackers pinpoint vulnerabilities in a system is by sending lots of requests to the application. Suppose our Django application doesn't have throttling implemented; then an attacker can send thousands of requests to our https://example.com/accounts/profiles/<ID>/
API endpoint, for example. This attacker can try different passwords, typically using brute force to guess a user ID that they can then use for other malicious activities.
Rate limiting or throttling is a way to limit the number of requests a user can send to an application. We can implement rate limiting on our Django REST framework application to help minimize the impact of automated attack tools.
# settings.py
REST_FRAMEWORK = {
...
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.UserRateThrottle",
"rest_framework.throttling.AnonRateThrottle"
],
"DEFAULT_THROTTLE_RATES": {
"anon": "3/hour",
"user": "350/day"
}
...
}
Third-party packages for rate limiting are django-ratelimit, django-throttle-requests, and django-defender. Accessing every URL resource in our application should involve validating the user and confirming that the user can perform the requested actions on the application resource. This is important to avoid mistakenly permitting a user who shouldn't have access to said resources.
As a matter of fact, deny all permissions and encourage the use of the principle of least privilege access. One of the ways we can counter these attacks is by always implementing server-side authentication and authorization. The first thing to remember is that we should never trust user data or any information coming from the browser.
Security, Security, Security
As we've seen, attackers are always looking for new ways to find or create vulnerabilities in a system. For the same reason, we need to preserve the integrity and security of our users' data. Security is as important as writing readable code. In conclusion, building Django applications with better access-level control is essential to preventing security breaches.