# PYTHON-DJANGO-SEC-030: Django SSRF via requests Library

> **Severity:** HIGH | **CWE:** CWE-918 | **OWASP:** A10:2021

- **Language:** Python
- **Category:** Django
- **URL:** https://codepathfinder.dev/registry/python/django/PYTHON-DJANGO-SEC-030
- **Detection:** `pathfinder scan --ruleset python/PYTHON-DJANGO-SEC-030 --project .`

## Description

This rule detects Server-Side Request Forgery (SSRF) vulnerabilities in Django
applications where untrusted user input from HTTP request parameters flows into
the URL argument of the requests library (requests.get(), requests.post(),
requests.put(), requests.delete(), requests.head(), requests.Session().get()).

SSRF occurs when an application fetches resources from a URL that an attacker
controls. The server-side HTTP request originates from inside the application's
network, allowing attackers to: access cloud provider metadata endpoints
(http://169.254.169.254/), reach internal services not exposed to the internet,
bypass network firewalls that trust the application server, scan the internal
network topology, and in some cases read sensitive files via file:// URLs.

In cloud environments (AWS, GCP, Azure), the instance metadata service is a
particularly dangerous target. An attacker can use SSRF to retrieve IAM credentials,
user-data scripts containing secrets, and other sensitive configuration from the
metadata endpoint.


## Vulnerable Code

```python
import requests

def fetch_url(request):
    url = request.GET.get('url')
    resp = requests.get(url)
    return resp.text
```

## Secure Code

```python
from django.http import JsonResponse
import requests
from urllib.parse import urlparse
import ipaddress

ALLOWED_DOMAINS = frozenset({'api.trusted-partner.com', 'cdn.example.com'})
BLOCKED_NETWORKS = [
    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'),  # Link-local / cloud metadata
    ipaddress.ip_network('127.0.0.0/8'),     # Loopback
    ipaddress.ip_network('::1/128'),          # IPv6 loopback
]

def is_safe_url(url):
    parsed = urlparse(url)
    if parsed.scheme not in ('http', 'https'):
        return False
    if not parsed.hostname:
        return False
    if parsed.hostname in ALLOWED_DOMAINS:
        return True
    try:
        ip = ipaddress.ip_address(parsed.hostname)
        return not any(ip in net for net in BLOCKED_NETWORKS)
    except ValueError:
        return False

def fetch_external_resource(request):
    url = request.GET.get('url', '')
    # SECURE: Validate URL against allowlist and blocked networks
    if not is_safe_url(url):
        return JsonResponse({'error': 'URL not permitted'}, status=400)
    response = requests.get(url, timeout=10, allow_redirects=False)
    return JsonResponse({'status': response.status_code, 'content': response.text[:1000]})

```

## Detection Rule (Python SDK)

```python
from rules.python_decorators import python_rule
from codepathfinder import calls, flows, QueryType
from codepathfinder.presets import PropagationPresets

class RequestsLib(QueryType):
    fqns = ["requests"]

_DJANGO_SOURCES = [
    calls("request.GET.get"),
    calls("request.POST.get"),
    calls("request.GET"),
    calls("request.POST"),
    calls("request.COOKIES.get"),
    calls("request.FILES.get"),
    calls("*.GET.get"),
    calls("*.POST.get"),
]


@python_rule(
    id="PYTHON-DJANGO-SEC-030",
    name="Django SSRF via requests Library",
    severity="HIGH",
    category="django",
    cwe="CWE-918",
    tags="python,django,ssrf,requests,OWASP-A10,CWE-918",
    message="User input flows to requests HTTP call. Validate and restrict URLs.",
    owasp="A10:2021",
)
def detect_django_ssrf_requests():
    """Detects Django request data flowing to requests library calls."""
    return flows(
        from_sources=_DJANGO_SOURCES,
        to_sinks=[
            RequestsLib.method("get", "post", "put", "delete", "patch",
                               "head", "options", "request").tracks(0),
        ],
        sanitized_by=[
            calls("urllib.parse.urlparse"),
            calls("validators.url"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Validate user-supplied URLs against a strict allowlist of trusted domains before making server-side HTTP requests.
- Block all requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback (127.0.0.0/8), and link-local (169.254.0.0/16) addresses.
- Restrict allowed URL schemes to http and https only, blocking file://, gopher://, ftp://, and other schemes.
- Set allow_redirects=False and validate redirect targets separately, as redirects can bypass initial URL validation.
- Consider using an egress proxy or network-level controls to restrict outbound HTTP requests to known-safe destinations regardless of application logic.

## Security Implications

- **Cloud Metadata Service Credential Theft:** Cloud providers expose instance metadata at http://169.254.169.254/ (AWS, GCP)
or http://169.254.169.254/metadata/ (Azure). An attacker who controls the URL
can make the application fetch its own IAM credentials, which can then be used
to access all AWS S3 buckets, RDS databases, and other cloud resources the
application role can access.

- **Internal Network Service Scanning and Access:** The application server typically has access to internal services like Redis,
Elasticsearch, databases, and admin panels that are not exposed to the internet.
SSRF enables attackers to send requests to these internal services, potentially
triggering unauthenticated operations on Redis (FLUSHALL), Elasticsearch
(index deletion), or internal admin endpoints.

- **Sensitive File Read via file:// Scheme:** Some HTTP client libraries honor file:// URLs, allowing SSRF to read local
files from the application server, including /etc/passwd, application secrets,
private keys, and Django settings files containing database credentials.

- **Security Control Bypass via Internal Endpoints:** Internal microservices often skip authentication for requests from trusted
internal IP ranges. SSRF bypasses perimeter security by making the trusted
application server send the request, allowing attackers to invoke privileged
internal API endpoints without credentials.


## FAQ

**Q: What makes SSRF particularly dangerous in cloud environments?**

Cloud providers expose instance metadata at a well-known link-local address
(169.254.169.254 on AWS and GCP). This service returns IAM credentials,
user-data scripts, and other sensitive configuration. An SSRF vulnerability
can fetch these credentials in a single request, giving attackers access to
all cloud resources the application role can access.


**Q: Can DNS rebinding bypass URL allowlist validation?**

Yes. DNS rebinding attacks involve a domain that initially resolves to a safe
IP but subsequently resolves to an internal IP after validation. To protect
against this, resolve the hostname to an IP at validation time and block requests
if the resolved IP falls in a protected range. Re-validate after any redirect.
Alternatively, use an egress proxy that enforces destination restrictions at
the network level.


**Q: Does setting a timeout on requests prevent SSRF?**

No. Timeout prevents the application from waiting too long for a response but
the request still reaches its destination. A short timeout reduces the window
for slow responses but does not prevent the attacker's target from processing
the request or returning credentials from the metadata service.


**Q: Should we block redirects to prevent SSRF bypass?**

Yes. Set allow_redirects=False and if your use case requires following redirects,
validate each redirect destination against the same URL policy as the original
request. Attackers use open redirectors on trusted domains to bypass URL
allowlists: the initial request goes to allowed-domain.com/redirect?url=169.254.169.254.


**Q: How do we handle legitimate use cases where users provide webhook URLs?**

Implement a webhook URL registration and verification flow: users register a URL
through a separate authenticated endpoint, the application sends a one-time
verification token to that URL to confirm ownership, and only verified URLs are
accepted for future webhook deliveries. This prevents SSRF while allowing
legitimate webhook functionality.


**Q: What is the difference between this rule and PYTHON-DJANGO-SEC-031?**

SEC-030 targets the popular third-party requests library, which is used by the
majority of Django applications that make outbound HTTP calls. SEC-031 targets
Python's standard library urllib.request.urlopen(). Both are SSRF vectors but
require separate sink patterns in the rule engine to detect.


**Q: Does this rule detect SSRF when the URL is stored in the database and fetched later?**

The current rule traces taint from HTTP request parameters in real-time flows.
Second-order SSRF (where a URL is stored from user input and later fetched) is
a separate detection challenge. Audit all places where database-stored URLs are
passed to requests calls as a complementary manual review step.


## References

- [CWE-918: Server-Side Request Forgery](https://cwe.mitre.org/data/definitions/918.html)
- [OWASP SSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)
- [OWASP Server-Side Request Forgery](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery)
- [PortSwigger SSRF Attacks](https://portswigger.net/web-security/ssrf)
- [requests library documentation](https://requests.readthedocs.io/en/latest/)
- [AWS IMDSv2 documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html)

---

Source: https://codepathfinder.dev/registry/python/django/PYTHON-DJANGO-SEC-030
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
