# PYTHON-FLASK-SEC-006: Flask SSRF via requests Library

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

- **Language:** Python
- **Category:** Flask
- **URL:** https://codepathfinder.dev/registry/python/flask/PYTHON-FLASK-SEC-006
- **Detection:** `pathfinder scan --ruleset python/PYTHON-FLASK-SEC-006 --project .`

## Description

This rule detects Server-Side Request Forgery (SSRF) in Flask applications where
user-controlled input from HTTP request parameters flows into outbound HTTP request
URLs via the requests library (requests.get, requests.post, requests.put,
requests.delete, requests.patch, requests.head, requests.options) or the standard
library (urllib.request.urlopen, urllib.request.Request).

SSRF allows attackers to instruct the application server to make HTTP requests to
destinations of the attacker's choosing. The server then acts as a proxy, forwarding
the response to the attacker. This is particularly severe in cloud environments
where the EC2/ECS/GKE metadata endpoint (169.254.169.254) returns instance
credentials without authentication when reached from the instance itself.

The rule uses taint analysis to trace the URL argument from Flask request sources
through variable assignments and function calls. The .tracks(0) parameter targets
the URL positional argument to requests functions, where SSRF occurs. Flows through
validate_url() or is_safe_url() are recognized as sanitizers because these functions
typically implement allowlist-based URL validation.


## Vulnerable Code

```python
# --- file: app.py ---
from flask import Flask, request
from services import fetch_remote_data

app = Flask(__name__)


@app.route('/proxy')
def proxy():
    url = request.args.get('url')
    data = fetch_remote_data(url)
    return data

# --- file: services.py ---
import requests as http_requests


def fetch_remote_data(endpoint):
    resp = http_requests.get(endpoint)
    return resp.text
```

## Secure Code

```python
from flask import Flask, request, abort
from urllib.parse import urlparse
import requests
import ipaddress

app = Flask(__name__)

ALLOWED_HOSTS = {'api.partner.com', 'cdn.example.com', 'webhook.example.org'}

def validate_url(url):
    """Allowlist-based URL validation. Raises ValueError on disallowed URLs."""
    parsed = urlparse(url)
    if parsed.scheme not in ('http', 'https'):
        raise ValueError(f'Scheme {parsed.scheme!r} not allowed')
    hostname = parsed.hostname
    if hostname not in ALLOWED_HOSTS:
        raise ValueError(f'Host {hostname!r} not in allowlist')
    # Block SSRF via DNS rebinding to private IPs
    try:
        ip = ipaddress.ip_address(hostname)
        if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
            raise ValueError('Private/reserved IP not allowed')
    except ValueError:
        pass  # hostname is not a raw IP -- DNS resolves at request time
    return url

@app.route('/fetch')
def fetch_resource():
    url = request.args.get('url', '')
    try:
        safe_url = validate_url(url)
    except ValueError as e:
        abort(400, str(e))
    # SAFE: URL validated against allowlist of known trusted hosts
    response = requests.get(safe_url, timeout=10)
    return response.text

```

## 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"]

class UrllibRequest(QueryType):
    fqns = ["urllib.request"]


@python_rule(
    id="PYTHON-FLASK-SEC-006",
    name="Flask SSRF via requests library",
    severity="HIGH",
    category="flask",
    cwe="CWE-918",
    tags="python,flask,ssrf,requests,OWASP-A10,CWE-918",
    message="User input flows to HTTP request URL. Validate and allowlist target URLs.",
    owasp="A10:2021",
)
def detect_flask_ssrf():
    """Detects Flask request data flowing to requests library calls."""
    return flows(
        from_sources=[
            calls("request.args.get"),
            calls("request.form.get"),
            calls("request.values.get"),
            calls("request.get_json"),
        ],
        to_sinks=[
            RequestsLib.method("get", "post", "put", "delete", "patch",
                               "head", "options", "request").tracks(0),
            UrllibRequest.method("urlopen", "Request").tracks(0),
            calls("http_requests.get"),
            calls("http_requests.post"),
        ],
        sanitized_by=[
            calls("*.validate_url"),
            calls("*.is_safe_url"),
        ],
        propagates_through=PropagationPresets.standard(),
        scope="global",
    )
```

## How to Fix

- Implement an allowlist of trusted hostnames that the application is permitted to request, and reject any URL whose host is not on the list.
- Resolve the DNS name to an IP address after allowlist validation and verify the resolved IP is not in a private, loopback, or link-local range to prevent DNS rebinding attacks.
- Disable redirects or validate redirect destinations against the allowlist -- requests follows redirects by default and a redirect to 169.254.169.254 bypasses the initial URL check.
- Use a dedicated HTTP client configured to reject private IP ranges at the network level (e.g., SSRF-aware proxy like Smokescreen) rather than relying solely on application-level validation.
- Set short timeouts and response size limits on outbound requests to limit damage from requests to slow internal services.

## Security Implications

- **Cloud Instance Credential Theft:** In AWS, GCP, and Azure, the instance metadata service is accessible at
http://169.254.169.254/ (or http://169.254.169.254/latest/meta-data/iam/
security-credentials/ on AWS). An attacker who controls the URL parameter
causes the Flask server to fetch its own IAM credentials and return them
in the response. These credentials give the attacker full API access to
the cloud account.

- **Internal Service Discovery and Exploitation:** The application server can reach internal services on the private network that
are not exposed to the internet: internal APIs, admin dashboards, Kubernetes
API servers, Redis instances, and Elasticsearch clusters. SSRF turns an external
attacker into an internal network attacker with the server's identity.

- **Port Scanning and Network Topology Mapping:** Timing differences in SSRF responses reveal which internal hosts and ports are
open. An attacker iterates RFC 1918 addresses and observes connection timeouts
vs. refused connections to map the internal network topology, identifying
targets for subsequent exploitation.

- **Bypassing IP-Based Access Controls:** Many internal services restrict access by source IP address, trusting requests
from the application server. SSRF bypasses this control because the request
originates from the trusted server. Admin interfaces that are secure against
external access become reachable via the vulnerable Flask endpoint.


## FAQ

**Q: Why is SSRF its own OWASP Top 10 category and not just another injection?**

SSRF was promoted to its own category in OWASP Top 10 2021 (A10) because of its
outsized impact in cloud-native architectures. Traditional injection attacks target
the application itself. SSRF weaponizes the application's network position -- the
server can reach internal services, cloud metadata endpoints, and private networks
that the attacker cannot touch directly. The cloud metadata attack alone justifies
a CRITICAL finding in AWS/GCP/Azure deployments.


**Q: Does disabling redirects in requests fix SSRF?**

Disabling redirects (allow_redirects=False) prevents the attacker from redirecting
through 169.254.169.254 after an initial allowed URL. But it does not prevent
SSRF when the initial URL itself is attacker-controlled. Both controls are
needed: allowlist validation of the initial URL AND redirect validation if
redirects are enabled.


**Q: How does DNS rebinding bypass URL allowlist checks?**

An attacker controls a domain on the allowlist (or tricks you into allowlisting
it) and sets its DNS TTL to 0. The validation resolves it to a legitimate IP.
By the time requests.get() resolves it for the actual connection, the attacker
has changed the DNS record to 169.254.169.254. The fix is to resolve DNS once,
validate the resolved IP, and use the IP for the actual connection.


**Q: Does this rule cover urllib as well as requests?**

Yes. The sink list includes urllib.request.urlopen() and urllib.request.Request()
in addition to all requests library methods. Both libraries are tracked because
real codebases mix them, especially when requests is not available or when
interacting with Python standard library code.


**Q: We have a URL-fetching feature that only accepts http/https URLs. Is that enough?**

Scheme validation is necessary but not sufficient. http://169.254.169.254/ is a
valid http:// URL. After validating the scheme, you must also validate the resolved
hostname against an allowlist and check that the resolved IP is not in a private
or reserved range.


**Q: PYTHON-FLASK-SEC-011 also covers SSRF. How are the two rules different?**

SEC-006 focuses on SSRF via the requests and urllib libraries -- the most common
Python HTTP clients. SEC-011 focuses on tainted URL host components specifically,
covering cases where the attacker controls only the hostname portion of a URL that
is otherwise application-constructed. Running both rules provides broader coverage
across different code patterns.


**Q: How do I test a confirmed finding in a safe environment?**

Use a local HTTP server on 127.0.0.1:8080 and supply http://127.0.0.1:8080/ as
the URL parameter. A successful response from your own server confirms the SSRF
is real. For cloud metadata testing, use a mock endpoint that returns a fake
credential response instead of the actual metadata service.


## 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 Testing for SSRF](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/19-Testing_for_Server-Side_Request_Forgery)
- [AWS IMDSv2 SSRF Protection](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html)
- [requests library documentation](https://requests.readthedocs.io/en/latest/)
- [Portswigger SSRF Tutorial](https://portswigger.net/web-security/ssrf)

---

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