DEV Community

Cover image for How To Build Secure Django Apps By Using Custom Middleware
Amr Saafan for Nile Bits

Posted on • Originally published at nilebits.com

How To Build Secure Django Apps By Using Custom Middleware

In today's digital world, when data breaches and cyber threats are more common than ever, developing safe online apps is essential. Django is a well-known and powerful web framework with integrated security measures. However, you might need to add more security levels as your program expands and its needs change. Using custom middleware is a great approach to improve the security of your Django application.

This blog post will explore how to create custom middleware to secure Django apps, focusing on adding multiple layers of security, from request validation to response handling. We will also look at various real-world scenarios, providing detailed code examples along the way.

What Is Middleware in Django?

In Django, middleware is a series of hooks that are executed before or after the request and response cycle. Middleware sits between the client request and the view response, making it an ideal place to apply security measures.

By creating custom middleware, we can intercept, process, or modify requests before they reach the view, as well as modify responses before they are sent back to the client.

Why Use Custom Middleware for Security?

Although Django comes with several middleware classes like SecurityMiddleware and CsrfViewMiddleware that handle common security aspects such as HTTPS enforcement and CSRF protection, custom middleware allows for:

Fine-grained request validation: Intercepting and analyzing requests at an early stage.

Custom security policies: Applying organization-specific or app-specific security rules.

Advanced logging and monitoring: Tracking request/response activity for auditing and compliance.

Rate limiting: Preventing abuse of the system through custom throttling mechanisms.

Data sanitization: Preventing malicious data from entering your application.

Now, let’s dive into how to build custom middleware for enhancing Django app security.

Setting Up a Django Project for Middleware

Before we begin building middleware, let's start by setting up a basic Django project. We will assume you already have Python and Django installed on your system.

Create a new Django project:

django-admin startproject secureapp
cd secureapp
python manage.py startapp custommiddleware

Enter fullscreen mode Exit fullscreen mode

Add the new app to INSTALLED_APPS in settings.py:

# settings.py
INSTALLED_APPS = [
    ...
    'custommiddleware',
]

Enter fullscreen mode Exit fullscreen mode

Ensure your Django app is running:

python manage.py migrate
python manage.py runserver

Now that your Django project is ready, let’s start building the custom middleware.

Example 1: Implementing a Request IP Whitelisting Middleware

One common security practice is to restrict access to your application based on IP address. This can be done by implementing a custom middleware to block all incoming requests except those from trusted IP addresses.

Step 1: Create a Middleware Class

In Django, middleware is just a Python class. Let’s create a new file called middleware.py inside the custommiddleware app and define the middleware class for IP whitelisting.

# custommiddleware/middleware.py

from django.http import HttpResponseForbidden

class IPWhitelistMiddleware:
    ALLOWED_IPS = ['127.0.0.1', '192.168.1.100']  # Replace with your allowed IP addresses

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

    def __call__(self, request):
        ip_address = request.META.get('REMOTE_ADDR')
        if ip_address not in self.ALLOWED_IPS:
            return HttpResponseForbidden("Access Denied: Your IP is not whitelisted.")
        return self.get_response(request)
Enter fullscreen mode Exit fullscreen mode

In this middleware:

We retrieve the IP address of the incoming request from request.META['REMOTE_ADDR'].

If the IP address is not in our whitelist (ALLOWED_IPS), we return a HttpResponseForbidden.

Step 2: Add Middleware to Django Settings

Once you’ve created the middleware, you need to add it to the MIDDLEWARE setting in settings.py.

# settings.py

MIDDLEWARE = [
    ...
    'custommiddleware.middleware.IPWhitelistMiddleware',
]
Enter fullscreen mode Exit fullscreen mode

Testing the Middleware

Try accessing your application from an IP not in the whitelist. You should see a 403 Forbidden response with the message "Access Denied: Your IP is not whitelisted."

Example 2: Implementing a Rate-Limiting Middleware

Rate limiting is an effective way to prevent abusive usage, such as brute force attacks or API abuse. Let’s implement a rate-limiting middleware that limits the number of requests a client can make in a given time period.

Step 1: Create a Rate-Limiting Middleware

We will store client requests in memory using a Python dictionary, where the key is the client’s IP address and the value is a tuple containing the number of requests and a timestamp.

# custommiddleware/middleware.py

import time
from django.http import HttpResponseTooManyRequests

class RateLimitMiddleware:
    RATE_LIMIT = 100  # Maximum number of requests allowed
    TIME_FRAME = 60 * 60  # Time frame in seconds (e.g., 1 hour)

    def __init__(self, get_response):
        self.get_response = get_response
        self.client_requests = {}

    def __call__(self, request):
        ip_address = request.META.get('REMOTE_ADDR')
        current_time = time.time()

        if ip_address in self.client_requests:
            requests, last_time = self.client_requests[ip_address]
            if current_time - last_time < self.TIME_FRAME:
                if requests >= self.RATE_LIMIT:
                    return HttpResponseTooManyRequests("Rate limit exceeded. Try again later.")
                else:
                    self.client_requests[ip_address] = (requests + 1, last_time)
            else:
                self.client_requests[ip_address] = (1, current_time)
        else:
            self.client_requests[ip_address] = (1, current_time)

        return self.get_response(request)

This middleware works as follows:

For each incoming request, we check if the IP address exists in the client_requests dictionary.

If the IP has exceeded the request limit within the specified time frame, we return a 429 Too Many Requests response.

Otherwise, we update the request count and timestamp.

Step 2: Add Middleware to Django Settings

# settings.py

MIDDLEWARE = [
    ...
    'custommiddleware.middleware.RateLimitMiddleware',
]

Enter fullscreen mode Exit fullscreen mode

Testing the Middleware

Send more than 100 requests from the same IP address within an hour, and you should see a 429 Too Many Requests error. You can adjust the RATE_LIMIT and TIME_FRAME values as per your requirements.

Example 3: Adding Custom Headers for Security

Another important security measure is to add security headers to HTTP responses, such as X-Frame-Options, Strict-Transport-Security, and Content-Security-Policy. Let’s create a middleware that adds these headers to the response.

Step 1: Create a Security Header Middleware

# custommiddleware/middleware.py

class SecurityHeadersMiddleware:

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

    def __call__(self, request):
        response = self.get_response(request)

        # Add security headers
        response['X-Frame-Options'] = 'DENY'
        response['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
        response['Content-Security-Policy'] = "default-src 'self'"

        return response
Enter fullscreen mode Exit fullscreen mode

In this middleware:

We add X-Frame-Options: DENY to prevent clickjacking.

Strict-Transport-Security enforces HTTPS.

Content-Security-Policy restricts the resources the browser is allowed to load.

Step 2: Add Middleware to Django Settings

# settings.py

MIDDLEWARE = [
    ...
    'custommiddleware.middleware.SecurityHeadersMiddleware',
]

Enter fullscreen mode Exit fullscreen mode

Testing the Middleware

After adding the middleware, inspect the HTTP response headers in your browser’s developer tools. You should see the newly added security headers.

Example 4: Implementing JWT Authentication Middleware

For API-based applications, securing endpoints using JWT (JSON Web Tokens) is common. While Django REST Framework provides a built-in mechanism for this, let's build a custom middleware to verify JWTs.

Step 1: Install PyJWT

First, install the pyjwt library to help decode and verify JWT tokens.

pip install pyjwt

Enter fullscreen mode Exit fullscreen mode

Step 2: Create JWT Authentication Middleware

# custommiddleware/middleware.py

import jwt
from django.conf import settings
from django.http import JsonResponse

class JWTAuthenticationMiddleware:

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

    def __call__(self, request):
        auth_header = request.headers.get('Authorization')

        if auth_header:
            try:
                token = auth_header.split(' ')[1]
                decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
                request.user = decoded_token['user_id']
            except (jwt.ExpiredSignatureError, jwt.DecodeError, jwt.InvalidTokenError):
                return JsonResponse({'error': 'Invalid token'}, status=401)

        return self.get_response(request)

Enter fullscreen mode Exit fullscreen mode

In this middleware:

We retrieve the JWT from the Authorization header.

We decode and verify the JWT using pyjwt.

If the token is valid, we attach the user ID to the request. Otherwise, we return a 401 Unauthorized error.

Step 3: Add Middleware to Django Settings

# settings.py

MIDDLEWARE = [
    ...
    'custommiddleware.middleware.JWTAuthenticationMiddleware',
]
Enter fullscreen mode Exit fullscreen mode

Testing the Middleware

Send a request to your application with a valid JWT token in the Authorization header. If the token is invalid or expired, you’ll get a 401 Unauthorized error.

Example 5: Implementing Request Data Sanitization Middleware

Data sanitization is a crucial aspect of web security. Malicious actors may attempt SQL injection or cross-site scripting (XSS) attacks by sending harmful data in requests. While Django provides protection against SQL injection and XSS through its ORM and templating system, you can still add another layer of defense by sanitizing incoming request data using custom middleware.

Step 1: Create Request Data Sanitization Middleware

This middleware will sanitize all input from request parameters (GET and POST) to ensure no malicious code is submitted to your application.

# custommiddleware/middleware.py

import re
from django.http import HttpResponseBadRequest

class DataSanitizationMiddleware:
    """Middleware to sanitize incoming request data."""

    def __init__(self, get_response):
        self.get_response = get_response
        # Regex to remove potentially harmful tags/scripts
        self.blacklist_patterns = [
            re.compile(r'<script.*?>.*?</script>', re.IGNORECASE),  # Block script tags
            re.compile(r'on\w+=".*?"', re.IGNORECASE),               # Block JS event handlers
            re.compile(r'javascript:', re.IGNORECASE)               # Block inline JS
        ]

    def sanitize(self, value):
        """Sanitize input value by removing harmful patterns."""
        for pattern in self.blacklist_patterns:
            value = re.sub(pattern, '', value)
        return value

    def sanitize_request_data(self, data):
        """Sanitize dictionary of request data."""
        sanitized_data = {}
        for key, value in data.items():
            if isinstance(value, list):  # Handle multiple values for the same key
                sanitized_data[key] = [self.sanitize(item) for item in value]
            else:
                sanitized_data[key] = self.sanitize(value)
        return sanitized_data

    def __call__(self, request):
        # Sanitize GET and POST data
        request.GET = request.GET.copy()
        request.GET.update(self.sanitize_request_data(request.GET))

        request.POST = request.POST.copy()
        request.POST.update(self.sanitize_request_data(request.POST))

        return self.get_response(request)
Enter fullscreen mode Exit fullscreen mode

In this middleware:

We define a set of regex patterns to block potentially harmful input like tags and JavaScript event handlers.</p> <p>We sanitize all incoming request data, including both GET and POST parameters, by stripping out malicious patterns.</p> <p>Step 2: Add Middleware to Django Settings<br> </p> <div class="highlight"><pre class="highlight plaintext"><code># settings.py MIDDLEWARE = [ ... 'custommiddleware.middleware.DataSanitizationMiddleware', ] </code></pre></div> <p></p> <p>Testing the Middleware</p> <p>Try submitting harmful scripts like <script>alert(&quot;XSS&quot;) or onclick="alert('XSS')" in your form data or query parameters. The middleware will sanitize the input, preventing the scripts from being executed.

Example 6: Implementing Cross-Origin Resource Sharing (CORS) Middleware

Cross-Origin Resource Sharing (CORS) is a security feature implemented in browsers to restrict how resources on one origin (domain) can be shared with another. If your Django app serves as an API backend, you may need to control CORS settings to prevent unauthorized access to your API from other domains.

While there are third-party libraries like django-cors-headers to handle CORS, let's build custom middleware to manage CORS settings.

Step 1: Create CORS Middleware

This middleware will inspect the Origin header of incoming requests and add the necessary CORS headers to the response.

custommiddleware/middleware.py

class CORSMiddleware:
"""Middleware to handle CORS (Cross-Origin Resource Sharing)."""

ALLOWED_ORIGINS = ['https://trusted-domain.com']

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

def __call__(self, request):
    response = self.get_response(request)
    origin = request.headers.get('Origin')

    # Check if the request's Origin is allowed
    if origin and origin in self.ALLOWED_ORIGINS:
        response['Access-Control-Allow-Origin'] = origin
        response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
        response['Access-Control-Allow-Headers'] = 'Authorization, Content-Type'
        response['Access-Control-Allow-Credentials'] = 'true'

    return response
Enter fullscreen mode Exit fullscreen mode

In this middleware:

We check if the Origin header of the incoming request is in our list of ALLOWED_ORIGINS.

If the origin is allowed, we add CORS-related headers to the response, such as Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers.

Step 2: Add Middleware to Django Settings

# settings.py

MIDDLEWARE = [
    ...
    'custommiddleware.middleware.CORSMiddleware',
]
Enter fullscreen mode Exit fullscreen mode

Testing the Middleware

Send an AJAX request from a trusted domain (such as https://trusted-domain.com), and the response should contain the necessary CORS headers. Requests from other domains will not have the Access-Control-Allow-Origin header, blocking cross-origin access.

Example 7: Implementing a Content Security Policy (CSP) Middleware

Content Security Policy (CSP) is a security standard designed to prevent various attacks like cross-site scripting (XSS) and data injection. By defining a strict CSP, you control which resources (e.g., scripts, styles) the browser is allowed to load, adding an additional layer of security.

Step 1: Create CSP Middleware

Let's build middleware that adds a strict CSP header to all responses.

# custommiddleware/middleware.py

class ContentSecurityPolicyMiddleware:
    """Middleware to add a Content Security Policy (CSP) header."""

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

    def __call__(self, request):
        response = self.get_response(request)

        # Define the Content-Security-Policy header
        response['Content-Security-Policy'] = (
            "default-src 'self'; "
            "script-src 'self' https://trusted-scripts.com; "
            "style-src 'self' https://trusted-styles.com; "
            "img-src 'self'; "
            "frame-ancestors 'none';"
        )

        return response
Enter fullscreen mode Exit fullscreen mode

In this middleware:

We define a strict Content-Security-Policy header.

This policy restricts scripts and styles to trusted domains and disallows any external frames from being embedded in the page.

Step 2: Add Middleware to Django Settings

# settings.py

MIDDLEWARE = [
    ...
    'custommiddleware.middleware.ContentSecurityPolicyMiddleware',
]
Enter fullscreen mode Exit fullscreen mode

Testing the Middleware

After adding the middleware, check the response headers in your browser’s developer tools. The Content-Security-Policy header should be present, and only resources from the specified domains will be loaded by the browser.

Example 8: Implementing a Custom Authentication Middleware

In many cases, Django's built-in authentication system works perfectly. However, you may want to integrate with an external authentication system or implement a completely custom authentication mechanism. Let’s build a custom authentication middleware that handles user login based on a custom token in the request.

Step 1: Create a Custom Authentication Middleware

This middleware will look for a custom authentication token in the request headers, validate it, and authenticate the user.

# custommiddleware/middleware.py

from django.contrib.auth.models import User
from django.http import JsonResponse

class CustomAuthenticationMiddleware:
    """Middleware to authenticate users using a custom token."""

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

    def __call__(self, request):
        auth_token = request.headers.get('X-Auth-Token')

        if auth_token:
            try:
                # Here you could validate the token (e.g., query the database or an external API)
                user = User.objects.get(auth_token=auth_token)
                request.user = user
            except User.DoesNotExist:
                return JsonResponse({'error': 'Invalid token'}, status=401)

        return self.get_response(request)

Enter fullscreen mode Exit fullscreen mode

In this middleware:

We check for the presence of an X-Auth-Token header in the request.

If the token is valid, we retrieve the corresponding user and attach it to the request. If not, we return a 401 Unauthorized response.

Step 2: Add Middleware to Django Settings

# settings.py

MIDDLEWARE = [
    ...
    'custommiddleware.middleware.CustomAuthenticationMiddleware',
]
Enter fullscreen mode Exit fullscreen mode

Testing the Middleware

Send a request to your application with a valid X-Auth-Token header. If the token is valid, the request will proceed; otherwise, you’ll receive a 401 Unauthorized response.

Final Thoughts on Securing Django Apps with Custom Middleware

As we've seen, custom middleware can significantly enhance the security of your Django application. Whether you’re whitelisting IPs, implementing rate limiting, securing responses with CSP headers, or building custom authentication systems, middleware provides a flexible, powerful way to secure every part of the request/response cycle.

Custom middleware allows you to integrate specific security policies tailored to your application's needs, adding an additional layer of protection that works alongside Django's built-in security mechanisms.

References

Django Documentation: Writing Your Own Middleware

OWASP: Cross-Site Scripting (XSS)

Mozilla Developer Network: Content Security Policy (CSP)

Top comments (0)