SSRF in Django Applications
Django applications commonly use the requests library or urllib to fetch external resources — importing data from URLs, validating webhooks, generating thumbnails, and unfurling links. Django's built-in URLValidator checks format but not destination, leaving applications vulnerable to SSRF when user-supplied URLs reach server-side HTTP calls.
Scan Your Django AppHow SSRF Manifests in Django
SSRF in Django typically appears in views that accept a URL and pass it to requests.get() or urllib.request.urlopen(). Common features include URL-based file imports, link preview generators, RSS feed fetchers, and webhook verification endpoints. Django's URLValidator validates URL syntax (protocol, hostname format) but does not resolve DNS or check whether the target is an internal IP address. Code like URLValidator()(user_url); requests.get(user_url) passes validation but still allows SSRF. Django REST Framework serializers with URLField validate format without blocking internal addresses. A serializer field url = serializers.URLField() accepts http://169.254.169.254/ as valid. Celery tasks that fetch URLs asynchronously are also vulnerable. Because the fetch happens in a background worker, developers sometimes skip validation, assuming the URL was checked earlier. If the URL is modified between validation and the task execution, SSRF is possible.
Real-World Impact
A Django application allowed users to import contacts from a CSV file URL. The view validated the URL format with URLValidator and fetched it with requests.get(). An attacker submitted http://169.254.169.254/latest/meta-data/iam/security-credentials/app-role, receiving AWS credentials that granted access to the application's RDS database. A Django-based CMS had a "fetch Open Graph data" feature for link previews. An attacker used it to scan the internal network by submitting URLs like http://10.0.0.1:8080, http://10.0.0.2:5432, observing which requests timed out versus which returned errors, mapping the internal infrastructure.
Step-by-Step Fix
Create a safe request utility
Build a wrapper around the requests library that resolves DNS and blocks private IPs before making any request.
# utils/safe_request.py
import ipaddress
import socket
import requests
from urllib.parse import urlparse
BLOCKED_NETWORKS = [
ipaddress.ip_network('127.0.0.0/8'),
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('169.254.0.0/16'),
]
def is_private_ip(hostname: str) -> bool:
try:
ip = ipaddress.ip_address(hostname)
except ValueError:
# Resolve hostname to IP
try:
ip = ipaddress.ip_address(socket.gethostbyname(hostname))
except socket.gaierror:
raise ValueError(f"Cannot resolve hostname: {hostname}")
return any(ip in network for network in BLOCKED_NETWORKS)
def safe_get(url: str, **kwargs) -> requests.Response:
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise ValueError(f"Blocked protocol: {parsed.scheme}")
if is_private_ip(parsed.hostname):
raise ValueError("Requests to private addresses are blocked")
kwargs.setdefault('timeout', 10)
kwargs.setdefault('allow_redirects', False)
return requests.get(url, **kwargs)Secure Django views that fetch URLs
Replace direct requests.get() calls with the safe wrapper in all views.
# views.py
from utils.safe_request import safe_get
from django.http import JsonResponse
def fetch_preview(request):
url = request.GET.get('url', '')
try:
response = safe_get(url)
# Parse Open Graph tags from response.text
return JsonResponse({'title': extract_og_title(response.text)})
except ValueError as e:
return JsonResponse({'error': str(e)}, status=400)
except requests.RequestException:
return JsonResponse({'error': 'Failed to fetch URL'}, status=502)Add a DRF validator for safe URLs
Create a custom serializer field that validates URLs against SSRF.
# serializers.py
from rest_framework import serializers
from utils.safe_request import is_private_ip
from urllib.parse import urlparse
class SafeURLField(serializers.URLField):
def to_internal_value(self, data):
url = super().to_internal_value(data)
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise serializers.ValidationError('Only HTTP(S) URLs are allowed.')
try:
if is_private_ip(parsed.hostname):
raise serializers.ValidationError('Internal URLs are not allowed.')
except ValueError as e:
raise serializers.ValidationError(str(e))
return url
class WebhookSerializer(serializers.Serializer):
url = SafeURLField()
events = serializers.ListField(child=serializers.CharField())Prevention Best Practices
1. Never trust Django's URLValidator alone for SSRF prevention — it only checks format, not destination. 2. Resolve DNS and verify IP addresses are not in private ranges before making requests. 3. Use an allowlist of permitted domains when possible. 4. Disable redirects in the requests library and validate each hop. 5. Run Django in a network segment with restricted access to internal services. 6. Set strict timeouts: requests.get(url, timeout=5).
How to Test
1. Submit http://169.254.169.254/latest/meta-data/ to any endpoint that fetches URLs and check the response for cloud metadata. 2. Test with http://127.0.0.1:8000/admin/ to see if the server fetches its own Django admin. 3. Try file:///etc/passwd to test protocol handling. 4. Set up a redirect server that 302-redirects to an internal IP and test it against each URL-fetching endpoint. 5. Use Vibe App Scanner to automatically detect SSRF vectors in your Django application.
Frequently Asked Questions
Does Django's URLValidator prevent SSRF?
No. Django's URLValidator only checks that the URL is syntactically correct (valid protocol, properly formatted hostname). It does not resolve DNS, check IP addresses, or block internal targets. http://169.254.169.254/ passes URLValidator because it is a valid URL.
Can SSRF happen through Django's ORM?
Not directly through the ORM, but if your application stores user-provided URLs in the database and later fetches them in Celery tasks or management commands, SSRF can occur. The vulnerability is in the code that makes HTTP requests, not in the ORM itself.
How do I prevent SSRF in Django Celery tasks?
Validate URLs both when they are submitted by the user and again when the Celery task executes the fetch. URLs stored in the database may have been valid at submission time but could resolve to different IPs later due to DNS changes. Always re-validate immediately before making the request.
Related Security Resources
Is Your Django App Vulnerable to SSRF?
VAS automatically scans for ssrf vulnerabilities in Django applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Django stack.