Description
Server-side request forgery (SSRF) occurs when an attacker causes a server to send unintended requests based on user input. This may expose internal networks or sensitive data that the attacker cannot reach directly.
Potential impact
- Internal network access: An attacker may reach internal services through the server.
- Data exposure: Data returned by internal systems may be disclosed.
- Service disruption: Malicious requests may interrupt service operation.
Remediation
- Validate input before making an outbound request.
- Prefer a server-owned mapping from identifiers to fixed URLs instead of accepting URLs from users.
- If URL input is necessary, use a standard parser to separate the scheme and hostname, then compare them exactly with a server-controlled allow-list.
- Disable automatic redirects or revalidate every destination. Block unexpected private or link-local DNS results and unintended proxy use.
Examples
Before
python
# User-controlled request destination
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route('/fetch')
def fetch():
url = request.args.get('url')
response = requests.get(url)
return response.content
After
python
# Fixed destinations with response limits
from flask import Flask, request, abort, Response
import requests
app = Flask(__name__)
@app.route('/fetch')
def fetch():
target_id = request.args.get('target')
# The user selects an identifier; fixed branches provide the actual URL.
if target_id == "status":
url = "https://status.example.com/health"
elif target_id == "catalog":
url = "https://api.example.com/v1/catalog"
else:
abort(400)
max_response_bytes = 1024 * 1024
chunks = []
total = 0
with requests.get(
url, timeout=3, allow_redirects=False, stream=True
) as response:
for chunk in response.iter_content(chunk_size=64 * 1024):
total += len(chunk)
if total > max_response_bytes:
abort(502)
chunks.append(chunk)
status_code = response.status_code
proxied = Response(
b''.join(chunks),
status=status_code,
content_type="application/octet-stream",
)
proxied.headers["X-Content-Type-Options"] = "nosniff"
return proxied
Explanation:
- Before: Unvalidated user input determines the outbound destination, allowing SSRF.
- After: An allowed identifier selects a server-owned fixed URL; other values are rejected. The code disables redirects, limits connection/read waits and caps the response at 1 MiB. It fixes the content type and
nosniffheader.timeout=3is not a three-second deadline for the entire operation, so configure any required overall deadline and network egress policy separately.