🔧 How To Build Secure Django Apps By Using Custom Middleware
Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to
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
Add the new app to INSTALLED_APPS in settings.py:
# settings.py
INSTALLED_APPS = [
...
'custommiddleware',
]
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)
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',
]
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',
]
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
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',
]
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
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)
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',
]
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)
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("XSS") 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
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',
]
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
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',
]
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)
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',
]
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)