SSRF in Flask Applications
Flask's minimalist design means developers build URL-fetching features from scratch using the requests library or urllib — often without SSRF protection. Since Flask provides no built-in URL validation or network-level safeguards, every endpoint that makes HTTP requests based on user input is a potential SSRF vector.
Scan Your Flask AppHow SSRF Manifests in Flask
SSRF in Flask appears wherever user input reaches an HTTP client call. The most common pattern is a route that accepts a URL parameter and calls requests.get(url) to fetch content for preview, import, or processing. Flask applications frequently build microservice architectures where services communicate over HTTP. If an endpoint constructs an internal service URL using user input (e.g., f"http://internal-api/{user_param}"), path traversal combined with SSRF lets attackers reach unintended endpoints. File download features that accept URLs instead of file uploads are another vector. A route like /download?url=... that fetches a file from a remote URL can be redirected to internal services. Flask-RESTful and Flask-RESTX APIs that accept URL fields in request bodies pass validation (URL format) but not destination checking, making SSRF straightforward through API calls.
Real-World Impact
A Flask microservice architecture had a PDF generation endpoint that fetched HTML from a user-provided URL and converted it to PDF. An attacker provided http://internal-billing-service:5000/api/invoices?all=true, and the PDF was generated from the billing service's response, exposing all customer invoices. A Flask application with an avatar URL feature allowed users to set their profile picture by URL. The server fetched the image and resized it. An attacker used this to scan the internal network by providing sequential internal IPs and measuring response times, discovering a Redis instance, a PostgreSQL server, and an internal admin panel.
Step-by-Step Fix
Build a safe requests wrapper for Flask
Create a utility module that validates URLs against SSRF before making any HTTP request.
# utils/safe_request.py
import ipaddress
import socket
from urllib.parse import urlparse
import requests
PRIVATE_NETS = [
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'),
ipaddress.ip_network('::1/128'),
]
def validate_url(url: str) -> str:
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise ValueError(f'Blocked protocol: {parsed.scheme}')
try:
resolved_ip = socket.gethostbyname(parsed.hostname)
except socket.gaierror:
raise ValueError(f'Cannot resolve: {parsed.hostname}')
ip = ipaddress.ip_address(resolved_ip)
for net in PRIVATE_NETS:
if ip in net:
raise ValueError('Requests to internal addresses are blocked')
return url
def safe_get(url: str, **kwargs) -> requests.Response:
validate_url(url)
kwargs.setdefault('timeout', 10)
kwargs.setdefault('allow_redirects', False)
return requests.get(url, **kwargs)Apply SSRF protection in Flask routes
Use the safe wrapper in every route that fetches user-provided URLs.
from flask import Flask, request, jsonify
from utils.safe_request import safe_get
app = Flask(__name__)
@app.route('/preview')
def link_preview():
url = request.args.get('url', '')
try:
response = safe_get(url)
return jsonify({'content_type': response.headers.get('content-type'),
'status': response.status_code})
except ValueError as e:
return jsonify({'error': str(e)}), 400
except Exception:
return jsonify({'error': 'Failed to fetch URL'}), 502Secure internal service URL construction
When building URLs for internal microservice calls, never interpolate user input into the hostname or port.
# UNSAFE - user controls the service target
@app.route('/api/data/<service>/<path:endpoint>')
def proxy_service(service, endpoint):
url = f'http://{service}:5000/{endpoint}' # SSRF!
return requests.get(url).json()
# SAFE - allowlist of internal services
SERVICE_MAP = {
'users': 'http://user-service:5000',
'billing': 'http://billing-service:5000',
}
@app.route('/api/data/<service>/<path:endpoint>')
def proxy_service(service, endpoint):
base = SERVICE_MAP.get(service)
if not base:
return jsonify({'error': 'Unknown service'}), 404
# Sanitize endpoint to prevent path traversal
safe_endpoint = endpoint.lstrip('/').split('?')[0]
url = f'{base}/{safe_endpoint}'
return requests.get(url, timeout=5).json()Prevention Best Practices
1. Validate all user-supplied URLs: check protocol, resolve DNS, and verify the IP is not in a private range. 2. Use an allowlist of permitted domains wherever possible. 3. Disable redirects and set strict timeouts when making HTTP requests. 4. Never construct internal service URLs using user input without strict validation. 5. Run Flask services in network-isolated environments where they cannot reach sensitive internal resources. 6. Use a dedicated HTTP client wrapper for all outbound requests that enforces SSRF checks.
How to Test
1. Identify all routes that accept URL parameters (query strings, JSON body fields, form data). 2. Submit http://169.254.169.254/latest/meta-data/ and check if cloud metadata is returned. 3. Test with http://localhost:5000/ and other common internal service ports. 4. Try redirect-based SSRF: host a page that 302-redirects to an internal address. 5. Use Vibe App Scanner to automatically detect SSRF vectors in your Flask application.
Frequently Asked Questions
Does Flask have any built-in SSRF protection?
No. Flask is a microframework that does not include URL validation, network filtering, or any SSRF prevention mechanisms. All protection must be implemented by the developer, typically through a safe HTTP client wrapper that validates URLs and blocks internal addresses before making requests.
Can SSRF happen through Flask file uploads?
SSRF through file uploads is indirect but possible. If your application accepts a URL instead of a file upload (e.g., "import from URL"), that URL fetch is an SSRF vector. Also, if uploaded files contain URLs that the server later fetches (like an SVG with external references), SSRF can occur during processing.
How do I handle redirects safely in Flask SSRF prevention?
Set allow_redirects=False when making requests, then manually check the Location header. Before following any redirect, validate the redirect target using the same SSRF checks (DNS resolution, private IP detection). Limit the maximum number of redirects to prevent infinite loops.
Related Security Resources
Is Your Flask App Vulnerable to SSRF?
VAS automatically scans for ssrf vulnerabilities in Flask applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Flask stack.