Skip to content

Instantly share code, notes, and snippets.

@kl4us
Last active March 3, 2025 08:39
Show Gist options
  • Save kl4us/0f3ee5f0bc71266d4dae279bf513a1e6 to your computer and use it in GitHub Desktop.
Save kl4us/0f3ee5f0bc71266d4dae279bf513a1e6 to your computer and use it in GitHub Desktop.
Django RFC1918 IP Restrictions

Django RFC1918 IP Restrictions

A Django middleware and URL helper functions to restrict access to specific URLs to only allow RFC1918 private IP addresses (intranet IPs).

Overview

This package provides:

  1. A middleware that checks if requests come from RFC1918 private IP ranges
  2. Helper functions to use in urls.py to easily protect specific URLs or URL patterns
  3. Support for both individual URL protection and prefix-based protection

Installation

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)

Usage

Add to settings.py

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

In urls.py

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')),
]

Features

  • 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

Security Notes

  1. 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.
  2. This middleware provides a convenient way to restrict access based on IP, but should not be the only security measure in place.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment