A Django middleware and URL helper functions to restrict access to specific URLs to only allow RFC1918 private IP addresses (intranet IPs).
This package provides:
- A middleware that checks if requests come from RFC1918 private IP ranges
- Helper functions to use in
urls.py
to easily protect specific URLs or URL patterns - Support for both individual URL protection and prefix-based protection
Create a file (e.g., middleware/rfc1918.py
) with the code below and add the middleware to your Django settings.
import ipaddress
from django.http import HttpResponseForbidden
from django.urls import path, re_path
from django.conf import settings
from typing import List, Callable, Any, Dict, Optional
class RFC1918Middleware:
"""
Django middleware that restricts access to specified URLs only to RFC1918 intranet IPs.
This middleware checks if the client's IP address belongs to one of the private
IP ranges defined in RFC1918 before allowing access to protected paths.
"""
def __init__(self, get_response):
self.get_response = get_response
# List of URL paths to protect
self.protected_paths = getattr(settings, 'RFC1918_PROTECTED_PATHS', [])
# List of RFC 1918 ranges (private networks)
self.rfc1918_ranges = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8') # Localhost for development
]
def __call__(self, request):
# Check if the current path is protected
path = request.path
if self._is_protected_path(path):
# Get the client's IP address
client_ip = self._get_client_ip(request)
# Check if the IP is allowed
if not self._is_rfc1918_ip(client_ip):
return HttpResponseForbidden("Access allowed only from intranet networks")
# If not protected or IP is allowed, proceed normally
return self.get_response(request)
def _is_protected_path(self, path: str) -> bool:
"""Check if the path is in the list of protected paths."""
# Check for exact match
if path in self.protected_paths:
return True
# Check for prefix match
for protected_path in self.protected_paths:
# If the protected path ends with *, check only the prefix
if protected_path.endswith('*') and path.startswith(protected_path[:-1]):
return True
return False
def _get_client_ip(self, request) -> str:
"""Extract the client's IP address from the request."""
if getattr(settings, 'USE_X_FORWARDED_FOR', False):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', '')
def _is_rfc1918_ip(self, ip_address: str) -> bool:
"""Check if the IP address belongs to RFC 1918 ranges."""
try:
ip_obj = ipaddress.ip_address(ip_address)
return any(ip_obj in network for network in self.rfc1918_ranges)
except ValueError:
return False
# Helper functions for urls.py
def rfc1918_path(route, view, kwargs=None, name=None) -> path:
"""
Version of django.urls.path that automatically adds the path to the protected list.
Use in urls.py exactly as you would use path().
Args:
route: URL pattern to match
view: View function to call when the pattern matches
kwargs: Additional arguments to pass to the view
name: Name for this URL pattern
Returns:
A path() object for use in urlpatterns
"""
# Add the path to the list of protected paths in settings
if not hasattr(settings, 'RFC1918_PROTECTED_PATHS'):
settings.RFC1918_PROTECTED_PATHS = []
# Add the path to the protected list
if route.endswith('/'):
settings.RFC1918_PROTECTED_PATHS.append('/' + route)
else:
settings.RFC1918_PROTECTED_PATHS.append('/' + route + '/')
# Also add the version without trailing slash
clean_route = '/' + route.rstrip('/')
if clean_route not in settings.RFC1918_PROTECTED_PATHS:
settings.RFC1918_PROTECTED_PATHS.append(clean_route)
# Return the normal path
return path(route, view, kwargs, name)
def rfc1918_path_prefix(prefix: str) -> None:
"""
Protects all paths that start with the specified prefix.
Example: rfc1918_path_prefix('admin')
Args:
prefix: URL prefix to protect
"""
if not hasattr(settings, 'RFC1918_PROTECTED_PATHS'):
settings.RFC1918_PROTECTED_PATHS = []
# Normalize the prefix
if not prefix.startswith('/'):
prefix = '/' + prefix
# Add the prefix with wildcard to the protected list
prefix_with_wildcard = prefix + '*'
if prefix_with_wildcard not in settings.RFC1918_PROTECTED_PATHS:
settings.RFC1918_PROTECTED_PATHS.append(prefix_with_wildcard)
MIDDLEWARE = [
# ... other middleware
'path.to.middleware.RFC1918Middleware',
# ... other middleware
]
# Optional: pre-define protected paths
RFC1918_PROTECTED_PATHS = [
'/admin/*',
'/private/',
]
# If you're behind a proxy
USE_X_FORWARDED_FOR = True # Only if you trust your proxy
from django.contrib import admin
from django.urls import path, include
from myapp.views import dashboard, api_view, public_view
from path.to.middleware import rfc1918_path, rfc1918_path_prefix
# Option 1: Protect individual URLs
urlpatterns = [
# URLs that require intranet IP
rfc1918_path('admin/', admin.site.urls),
rfc1918_path('dashboard/', dashboard, name='dashboard'),
rfc1918_path('api/private/', api_view, name='private_api'),
# Public URLs (use normal path)
path('public/', public_view, name='public'),
]
# Option 2: Protect entire URL prefixes
rfc1918_path_prefix('admin') # Protects all URLs starting with /admin/
rfc1918_path_prefix('api/private') # Protects all URLs starting with /api/private/
# Rest of URLs follow normal pattern
urlpatterns += [
path('other/', include('other_app.urls')),
]
- Restricts access to specified URLs to RFC1918 private IP ranges only:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 127.0.0.0/8 (localhost for development)
- Support for X-Forwarded-For header for use behind proxies
- Protection for individual URLs via
rfc1918_path()
- Protection for URL patterns via
rfc1918_path_prefix()
- Returns 403 Forbidden for unauthorized IPs
- If your Django application is behind a proxy, make sure to set
USE_X_FORWARDED_FOR = True
in your settings and ensure your proxy is properly configured to pass the correct client IP. - This middleware provides a convenient way to restrict access based on IP, but should not be the only security measure in place.