Best Django security practices

Learn to secure your Django applications effectively with our expert hands-on tutorial.

Best Django security practices

Want to know how to protect your Django applications? Dive into our latest blog post, where we guide you through the best practices for Django security, a powerful and user-friendly Python web framework. Explore how these techniques can not only enhance the security of your web applications but also bring tangible benefits to your development journey.

In this guide, Escape's security research team has gathered the most crucial tips to protect your Django applications from potential breaches, including how to secure Django REST APIs. Our goal is to empower you to create more resilient and efficient Django apps. Let's get started!

What is Django?

Django is a high-level, open-source web framework written in Python that encourages rapid development and clean, pragmatic design. It follows the Model-View-Controller (MVC) architectural pattern and emphasizes reusability, less code, and the "Don't Repeat Yourself" (DRY) principle. Django simplifies the process of building web applications by providing built-in features for database management, authentication, URL routing, and templating, allowing developers to focus on application logic rather than boilerplate code.

Is Django good for security?

Yes, Django is widely recognized as a secure web framework. It incorporates built-in security features to protect against common vulnerabilities such as Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF), and SQL injection. With a robust authentication system, ORM protection, and other security measures, Django provides a solid foundation for developing secure web applications. Additionally, the active Django community (18 years old!) and regular updates contribute to maintaining a high level of security.

💡
Want to improve your penetration testing skills? Try your skills out on the vulnerable Django App.

django.nV comes with a series of writeups for the vulnerabilities we've added to the code. Each tutorial comes with a description of the vuln, a hint to where to find it, and then the exact bug and how it could be remedied.

Useful security libraries for Django

Django has a strong security foundation, and there are several external libraries that can further enhance the security of Django applications. Here is a detailed list of some useful security libraries for Django:

  1. django-allauth: Adds features for account management, including secure registration, authentication, and password management.
  2. django-csp: Implements Content Security Policy (CSP) headers to mitigate the risk of Cross-Site Scripting (XSS) attacks.
  3. django-defender: Blocks people from brute forcing login attempts ( The big difference from similar django-axes that they use redis instead of the database to store state, which greatly improves the speed of the checks on login)
  4. django-ratelimit (recommended by OWASP): a ratelimiting decorator for Django views, storing rate data in the configured Django cache backend.
  5. django-honeypot: Generic honeypot utilities for use in Django projects.$
  6. django-secured-fields: Django encrypted fields with search enabled.
  7. django-guardian: Allows object-level permissions for Django models, providing a finer level of control over user access to specific database records
  8. django-guardian support for Django REST Framework
  9. django-cryptography: Easily encrypt data in Django
  10. django-debug-toolbar: While primarily a debugging tool, it can help identify security-related issues during development by displaying various information about the request/response cycle.
💡
Ensure that the application is never in DEBUG mode in a production environment. Never run DEBUG = True in production.

How to secure Django REST APIs?

The OWASP Top 10 2023 list is a great starting point to enhance the security of your Django applications.

It is especially valuable since it covers a broad spectrum of the most common and impactful security vulnerabilities. More than that, one of the key strengths of the OWASP Top 10 is its regular updates. By aligning your Django security with the OWASP Top 10, you ensure that you're protected against the latest and most critical vulnerabilities. According to OWASP, your approach to securing your web API should be to start at the top threat and work down, it will ensure that any time spent on security will be spent most effectively.

For vulnerabilities listed in OWASP Top 10 2019 you can check OWASP REST framework cheatsheet. Some names have changed since then (like Unsafe Consumption of APIs replaced Insufficient Logging & Monitoring). We'll focus on 3 updates since OWASP Top 10 2019: Broken Object Property Level Authorization, Unrestricted Resource Consumption, and Unrestricted Access to Sensitive Business Flows.

To secure against BOPLA (a combination of Excess Data Exposure and Mass Assignment), you can:

  • Implement testing strategies that validate field-level permissions
  • Review the serializer and the information you are displaying. If the serializer is inheriting from ModelSerializer DO NOT use the exclude Meta property.
  • When using ModelForms, us Meta.fields (allow list approach). Don't use Meta.exclude (block list approach) or ModelForms.Meta.fields = "__all__"

To secure against Unrestricted Resource Consumption, you can:

  • Throttle your API: Configure the setting DEFAULT_THROTTLE_CLASSES.
  • Limit query complexity and depth.
  • Use django-ratelimit library. If possible rate limiting should also be done with a WAF or similar. DRF should be the last layer of rate limiting.
  • Test your system regularly.

To secure against Unrestricted Access to Sensitive Business Flows, you can:

  • Use the power of captchas or even biometric solutions like typing patterns. You can use django-recaptcha library to protect against automated bot attacks on forms.
  • Consider blocking IP addresses of Tor exit nodes and well-known proxies. Create a new Django middleware to intercept incoming requests before they reach the view. Middleware allows you to process requests and responses globally for your Django application. Include your custom middleware in the MIDDLEWARE setting in your Django project's settings:
# custom_middleware.py

from django.http import HttpResponseForbidden
from ipaddress import ip_address

class BlockTorAndProxiesMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        # List of Tor exit nodes and well-known proxies
        blocked_ips = ['192.168.1.1', '192.168.1.2', ...]

        # Get the source IP address from the request
        client_ip = ip_address(request.META.get('REMOTE_ADDR', None))

        # Check if the client IP is in the blocked IPs list
        if client_ip in blocked_ips:
            # Optionally, log the blocked request
            # logger.warning(f"Blocked request from {client_ip}")

            # Return a forbidden response or take other actions as needed
            return HttpResponseForbidden("Access denied")

        return self.get_response(request)

Other best practices for Django security

Authentication

Authentication is essential for building a secure Django app. With Django's built-in authentication system, only authorized users can access specific parts of your site or perform certain actions.

Let's start by creating a user registration form where users can create their accounts. Here's a simple example of how you can do that:

from django.contrib.auth.forms import UserCreationForm

def register(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            # Redirect to a success page or do something else
    else:
        form = UserCreationForm()
    return render(request, 'registration/register.html', {'form': form})

Once the user is registered, we can use the authenticate() function to check their credentials when they try to log in. Here's how it works:

from django.contrib.auth import authenticate, login

def login_view(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            # Redirect to a success page or do something else
        else:
            # Show an error message or do something else
    else:
        # Show the login form
    return render(request, 'login.html')

Once the user is successfully authenticated, we can grant them access to protected resources. Django provides a useful decorator called login_required to make sure that only authenticated users can access certain views. Here's how it looks:

from django.contrib.auth.decorators import login_required

@login_required
def protected_view(request):
    # Do something with the protected resource
    return render(request, 'protected.html')

Lastly, it's really important to store user passwords securely. Django takes care of this by hashing them with its built-in password-hashing mechanism. So you don't have to worry about implementing it yourself. Just make sure to follow these practices, and your user authentication in your Django application will be secure and reliable.

Multi-factor authentication

To keep your user accounts safe, it's important to have multi-factor authentication (MFA) in place. MFA adds an extra layer of security by requiring multiple forms of identification, such as a password and a verification code. In Django, enabling MFA is simple. Just use the built-in authentication system and packages like django-otp.

Here's an example of how to enable MFA for a user in Django:

from django.contrib.auth.decorators import login_required
from django_otp.decorators import otp_required

@login_required
@otp_required
def dashboard(request):
    # Code for the protected dashboard page

In this code snippet, the @login_required decorator makes sure the user is authenticated, and the @otp_required decorator enforces the use of MFA. With this setup, users will have to provide both their credentials and a verification code to access the protected dashboard. Make sure to choose reliable MFA methods and educate your users on the importance of keeping their authentication factors secure.

Social media authentication

Here's the code snippet that illustrates the concept of Social Media Authentication in Django:

# Install the Django-allauth library
pip install django-allauth

# Configure social media authentication in settings.py
INSTALLED_APPS = [
    ...
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.facebook',
    'allauth.socialaccount.providers.twitter',
    'allauth.socialaccount.providers.google',
]

AUTHENTICATION_BACKENDS = [
    ...
    'allauth.account.auth_backends.AuthenticationBackend',
]

# Add social media authentication buttons
{% load socialaccount %}
<a href="{% provider_login_url 'facebook' %}">Log in with Facebook</a>
<a href="{% provider_login_url 'twitter' %}">Log in with Twitter</a>
<a href="{% provider_login_url 'google' %}">Log in with Google</a>

After enabling social media authentication, you can use the returned user object to create or log in the user:

from allauth.socialaccount.models import SocialAccount

def authenticate_social_media_user(request):
    # Retrieve social media user data
    social_account = SocialAccount.objects.get(user=request.user)
    
    # Create or log in the user in your Django application
    user, created = User.objects.get_or_create(username=social_account.user.username, email=social_account.user.email)
    
    if created:
        # Perform additional actions for newly created user
        pass
    
    # Log in the user
    login(request, user)
    
    return redirect('home')

Remember to handle security considerations properly when dealing with tokens and user data from third-party platforms to ensure a secure authentication process.

Authorization

Once the user is authenticated, you can implement authorization to control what they can do in your application. Django provides a flexible permission system. You can create custom permissions using the Permission model and assign them to users or groups. For instance, you can create a permission called "can_edit_posts" and assign it to a group of users who can modify posts. To check if a user has a specific permission, use the has_perm method. Here's an example:

if user.has_perm('myapp.can_edit_posts'):
    # Allow the user to edit posts
    ...
else:
    # Restrict access to post editing functionality
    ...

By correctly implementing authentication and authorization, you can ensure that only authorized users can access sensitive information or perform certain actions in your Django application, making it more secure.

Role-based authorization

Role-based Authorization in Django is all about controlling access to different parts of an application based on user roles or permissions. By defining roles and assigning permissions accordingly, we can make sure only authorized users can do certain things.

Let's say we have an e-commerce site with three roles: customer, staff, and admin. We can limit certain actions, like editing product details or managing user accounts, to staff and admin roles only.

Django makes implementing role-based authorization easy with its built-in authentication and authorization system. We can use decorators like login_required and user_passes_test to restrict views and limit access based on roles.

One great thing about Django is its flexible user model, which lets us add custom fields and methods to support role-based authorization even better.

Group-based authorization

Group-based authorization in Django allows you to control access to specific parts of your website based on a user's group membership. By associating users with groups and using the Group model, you can easily manage permissions and determine what actions different groups are allowed to perform. For example, you can create an "Admin" group with special privileges using code like this:

from django.contrib.auth.models import Group

admin_group, _ = Group.objects.get_or_create(name='Admin')

To implement group-based authorization, you can use the @group_required decorator. This decorator is applied to a view function or class and ensures that only users who belong to a specific group can access it. For example, if you have a view that only allows members of the "Admin" group to add or delete content, you can use code like this:

from django.contrib.auth.decorators import login_required
from myapp.decorators import group_required

@login_required
@group_required('Admin')
def add_delete_content(request):
    # Logic to add or delete content

With group-based authorization, you can ensure that your website remains secure and only authorized users can perform certain actions. By defining different groups with different levels of access, you can easily manage permissions and control what actions each group can take.

Attribute-based authorization

Using attribute-based authorization in Django allows you to control access to specific resources based on attributes or properties of the user. Instead of relying solely on roles or groups, attribute-based authorization provides granular control over permissions. For example, you can allow users with a certain attribute, such as being an admin or having a specific role, to perform certain actions or access certain views. This can be achieved by using Django's built-in decorators.

from django.contrib.auth.decorators import user_passes_test

def is_admin(user):
    return user.is_authenticated and user.is_superuser

@user_passes_test(is_admin)
def admin_view(request):
    # Only admins can access this view
    # Your code here

By implementing attribute-based authorization, you can ensure that only users with specific attributes are granted access to certain resources in your Django application. This provides an additional layer of security and control over user permissions, ultimately enhancing the overall security of your application.

Input validation and output encoding

Common input validation techniques

To ensure security in Django, it's important to validate user inputs using common techniques. This helps to protect against common security vulnerabilities such as SQL injection or cross-site scripting (XSS) attacks. One technique is using regular expressions to match specific patterns in the input. For example, to validate a phone number in the format (XXX) XXX-XXXX, we can use a regular expression like this:

import re

phone_number = "(123) 456-7890"
pattern = r"\(\d{3}\)\s\d{3}-\d{4}"

if re.match(pattern, phone_number):
    print("Valid phone number")
else:
    print("Invalid phone number")

Another technique is whitelisting, where we only accept input that consists of allowed characters. For example, if we define a whitelist of alphanumeric characters, we can check if the input only contains these characters:

allowed_characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
user_input = "abc123"

if all(c in allowed_characters for c in user_input):
    print("Valid input")
else:
    print("Invalid input")

Additionally, it's important to validate the length of the input to prevent security issues like buffer overflows. We can set a maximum limit for input length and check if the input exceeds that limit:

max_length = 10
user_input = "Hello, world!"

if len(user_input) <= max_length:
    print("Input within limit")
else:
    print("Input exceeds limit")

By implementing these validation techniques, we can enhance the security of our Django application.

Best practices for output encoding

Using proper output encoding is crucial for maintaining security in Django. By correctly encoding output, we can protect against various types of malicious attacks, such as cross-site scripting (XSS). One best practice for output encoding is to always use Django's built-in template engine and its automatic HTML escaping feature. This can be done by using the {{ variable }} syntax in templates, which automatically escapes any HTML tags present in the variable's value, preventing them from being rendered as actual HTML. Additionally, if you are manually rendering HTML in your code, you should use Django's mark_safe function to explicitly mark the output as safe, ensuring it won't be escaped.

from django.utils.safestring import mark_safe

def render_html():
    html = '<script>alert("Intrusion attempt!")</script>'
    return mark_safe(html)

By following these best practices, we can ensure that our Django applications defend against potential security vulnerabilities resulting from improper output encoding.

Secure configuration and deployment

To keep your database credentials safe, avoid hardcoding them directly into your settings file. Instead, store them in a separate configuration file or use environment variables. For example, you can define your database configuration in a file called config.py and then import it into your settings.py file like this:

# config.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'HOST': 'localhost',
        'PORT': '5432',
        'NAME': 'mydatabase',
        'USER': 'db_user',
        'PASSWORD': 'db_password',
    }
}

# settings.py
from config import DATABASES

By separating sensitive information from your code, you reduce the risk of accidental exposure and make it easier to update your credentials securely.

To protect sensitive data during communication, use HTTPS. In Django, you can enable HTTPS by configuring your web server and adding a few settings to your settings.py file:

# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

These settings ensure that cookies and session data are only transmitted over secure connections, adding an extra layer of security.

When deploying your Django application, it's important to secure your static and media files. Instead of relying on Django's development server, use a separate web server, such as Nginx or Apache, to serve these files. This allows you to take advantage of the server's security features and implement access restrictions. Django also provides built-in file storage classes that offer features like permission control, file encryption, and integration with CDNs. Using classes like FileSystemStorage or S3Boto3Storage enhances the security and performance of your file handling.

To ensure secure configuration and deployment in Django, follow these key steps:

  1. Keep secret keys and sensitive configuration settings separate from version control. Use environment variables or a separate configuration file that is ignored by your version control system:
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
  1. Regularly update Django and its dependencies to address any security vulnerabilities:
pip install --upgrade django
  1. Deploy your Django application using HTTPS to encrypt transmitted data between the server and clients. Obtain an SSL certificate and configure your web server:
# Nginx configuration
server {
    listen 443 ssl;
    ssl_certificate /path/to/ssl_certificate;
    ssl_certificate_key /path/to/ssl_certificate_key;
    ...
}

By following these steps, you can enhance the security of your Django application and protect sensitive user data.

Conclusion

In conclusion, securing applications built with Django is a multifaceted task requiring a comprehensive approach to mitigate potential risks and ensure the confidentiality, integrity, and availability of data. By implementing robust authentication and authorization mechanisms, input validation, output encoding, and rate limiting, developers can establish a solid foundation for API protection. Leveraging Django libraries such as django-ratelimit, django-defender, django-honeypot, django-secured-fields or django-guardian streamlines the integration of these security measures into Django applications.

It is essential to tailor security strategies to the specific needs of your API, considering factors like the nature of data exchanged, user authentication requirements, and the level of exposure to potential threats. Regular security audits, automated API security testing, and staying informed about emerging security practices within the Django community contribute to an adaptive and resilient security posture.

Want to learn more? 💡