cursorrules / rules
ivangrynenko/cursorrules/.cursor/rules/python-ssrf.mdc
Detect and prevent Server-Side Request Forgery (SSRF) vulnerabilities in Python applications as defined in OWASP Top 10:2021-A10
Cursor rule88 starsChanged 11 months ago
---
description: Detect and prevent Server-Side Request Forgery (SSRF) vulnerabilities in Python applications as defined in OWASP Top 10:2021-A10
globs: *.py
alwaysApply: false
---
# Python Server-Side Request Forgery (SSRF) Standards (OWASP A10:2021)
This rule enforces security best practices to prevent Server-Side Request Forgery (SSRF) vulnerabilities in Python applications, as defined in OWASP Top 10:2021-A10.
<rule>
name: python_ssrf
description: Detect and prevent Server-Side Request Forgery (SSRF) vulnerabilities in Python applications as defined in OWASP Top 10:2021-A10
filters:
- type: file_extension
pattern: "\\.py$"
- type: file_path
pattern: ".*"
actions:
- type: enforce
conditions:
# Pattern 1: Detect direct use of requests library with user input
- pattern: "requests\\.(get|post|put|delete|head|options|patch)\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used directly in HTTP requests. Implement URL validation and allowlisting."
# Pattern 2: Detect urllib usage with user input
- pattern: "urllib\\.(request|parse)\\.\\w+\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used directly in urllib functions. Implement URL validation and allowlisting."
# Pattern 3: Detect http.client usage with user input
- pattern: "http\\.client\\.\\w+\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used directly in http.client functions. Implement URL validation and allowlisting."
# Pattern 4: Detect aiohttp usage with user input
- pattern: "aiohttp\\.\\w+\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used directly in aiohttp functions. Implement URL validation and allowlisting."
# Pattern 5: Detect httpx usage with user input
- pattern: "httpx\\.\\w+\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used directly in httpx functions. Implement URL validation and allowlisting."
# Pattern 6: Detect pycurl usage with user input
- pattern: "pycurl\\.\\w+\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used directly in pycurl functions. Implement URL validation and allowlisting."
# Pattern 7: Detect subprocess calls with user input that might lead to SSRF
- pattern: "subprocess\\.(Popen|call|run|check_output|check_call)\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used in subprocess calls, which might lead to SSRF. Validate and sanitize input."
# Pattern 8: Detect os.system calls with user input that might lead to SSRF
- pattern: "os\\.(system|popen|spawn)\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used in OS commands, which might lead to SSRF. Validate and sanitize input."
# Pattern 9: Detect URL construction with user input
- pattern: "(f|r)[\"\']https?://[^\"\']*?\\{[^\\}]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used in URL construction. Implement URL validation and allowlisting."
# Pattern 10: Detect URL joining with user input
- pattern: "urljoin\\([^,]+,[^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used in URL joining. Implement URL validation and allowlisting."
# Pattern 11: Detect file opening with user input (potential local SSRF)
- pattern: "open\\([^,]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential local SSRF vulnerability detected. User-controlled input is being used in file operations. Validate file paths and use path sanitization."
# Pattern 12: Detect XML/YAML parsing with user input (potential XXE leading to SSRF)
- pattern: "(ET\\.fromstring|ET\\.parse|ET\\.XML|minidom\\.parse|parseString|yaml\\.load)\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential XXE vulnerability that could lead to SSRF detected. User-controlled input is being used in XML/YAML parsing. Use safe parsing methods and disable external entities."
# Pattern 13: Detect socket connections with user input
- pattern: "socket\\.(socket|create_connection)\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used in socket connections. Implement host/port validation and allowlisting."
# Pattern 14: Detect FTP connections with user input
- pattern: "ftplib\\.FTP\\([^)]*?\\b(request\\.\\w+|params\\[\\'[^\\']+\\'\\]|data\\[\\'[^\\']+\\'\\]|json\\[\\'[^\\']+\\'\\]|args\\.get|form\\.get)"
message: "Potential SSRF vulnerability detected. User-controlled input is being used in FTP connections. Implement host validation and allowlisting."
# Pattern 15: Detect missing URL validation before making requests
- pattern: "def\\s+\\w+\\([^)]*?\\):[^\\n]*?\\n(?:[^\\n]*?\\n)*?[^\\n]*?requests\\.(get|post|put|delete|head|options|patch)\\([^)]*?url\\s*=\\s*[^\\n]*?(?!.*?validate_url)"
message: "Missing URL validation before making HTTP requests. Implement URL validation with allowlisting to prevent SSRF attacks."
- type: suggest
message: |
**Python Server-Side Request Forgery (SSRF) Prevention Best Practices:**
1. **URL Validation and Allowlisting:**
- Implement strict URL validation
- Use allowlists for domains, IP ranges, and protocols
- Example implementation:
```python
import re
import socket
import ipaddress
from urllib.parse import urlparse
def is_valid_url(url, allowed_domains=None, allowed_protocols=None, block_private_ips=True):
"""
Validate URLs against allowlists and block private IPs.
Args:
url (str): The URL to validate
allowed_domains (list): List of allowed domains
allowed_protocols (list): List of allowed protocols
block_private_ips (bool): Whether to block private IPs
Returns:
bool: True if URL is valid according to rules
"""
if not url:
return False
# Default allowlists if none provided
if allowed_domains is None:
allowed_domains = ["example.com", "api.example.com"]
if allowed_protocols is None:
allowed_protocols = ["https"]
try:
# Parse URL
parsed_url = urlparse(url)
# Check protocol
if parsed_url.scheme not in allowed_protocols:
return False
# Check domain against allowlist
if parsed_url.netloc not in allowed_domains:
return False
# Block private IPs if enabled
if block_private_ips:
hostname = parsed_url.netloc.split(':')[0]
try:
ip_addresses = socket.getaddrinfo(
hostname, None, socket.AF_INET, socket.SOCK_STREAM
)
for family, socktype, proto, canonname, sockaddr in ip_addresses:
ip = sockaddr[0]
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_reserved:
return False
except socket.gaierror:
# DNS resolution failed
return False
return True
except Exception:
return False
# Usage example
def fetch_resource(resource_url):
if not is_valid_url(resource_url):
raise ValueError("Invalid or disallowed URL")
# Proceed with request
import requests
return requests.get(resource_url)
```
2. **Implement Network-Level Controls:**
- Use network-level allowlists
- Configure firewalls to block outbound requests to internal resources
- Example with proxy configuration:
```python
import requests
def safe_request(url):
# Configure proxy that implements URL filtering
proxies = {
'http': 'http://ssrf-protecting-proxy:8080',
'https': 'http://ssrf-protecting-proxy:8080'
}
# Set timeout to prevent long-running requests
timeout = 10
try:
return requests.get(url, proxies=proxies, timeout=timeout)
except requests.exceptions.RequestException as e:
# Log the error and handle gracefully
logging.error(f"Request failed: {e}")
return None
```
3. **Use Safe Libraries and Wrappers:**
- Create wrapper functions for HTTP requests
- Implement consistent security controls
- Example wrapper:
```python
import requests
from urllib.parse import urlparse
class SafeRequestHandler:
def __init__(self, allowed_domains=None, allowed_protocols=None):
self.allowed_domains = allowed_domains or ["api.example.com"]
self.allowed_protocols = allowed_protocols or ["https"]
def validate_url(self, url):
parsed_url = urlparse(url)
# Validate protocol
if parsed_url.scheme not in self.allowed_protocols:
return False
# Validate domain
if parsed_url.netloc not in self.allowed_domains:
return False
return True
def request(self, method, url, **kwargs):
if not self.validate_url(url):
raise ValueError(f"URL validation failed for: {url}")
# Set sensible defaults
kwargs.setdefault('timeout', 10)
# Make the request
return requests.request(method, url, **kwargs)
def get(self, url, **kwargs):
return self.request('GET', url, **kwargs)
def post(self, url, **kwargs):
return self.request('POST', url, **kwargs)
# Usage
safe_requests = SafeRequestHandler()
response = safe_requests.get('https://api.example.com/data')
```
4. **Disable Redirects or Implement Redirect Validation:**
- Disable automatic redirects
- Validate each redirect location
- Example:
```python
import requests
def safe_request_with_redirect_validation(url, allowed_domains):
# Disable automatic redirects
session = requests.Session()
response = session.get(url, allow_redirects=False)
# Handle redirects manually with validation
redirect_count = 0
max_redirects = 5
while 300 <= response.status_code < 400 and redirect_count < max_redirects:
redirect_url = response.headers.get('Location')
# Validate redirect URL
parsed_url = urlparse(redirect_url)
if parsed_url.netloc not in allowed_domains:
raise ValueError(f"Redirect to disallowed domain: {parsed_url.netloc}")
# Follow the redirect with validation
redirect_count += 1
response = session.get(redirect_url, allow_redirects=False)
return response
```
5. **Use Metadata Instead of Direct URLs:**
- Use resource identifiers instead of URLs
- Resolve identifiers server-side
- Example:
```python
def fetch_resource_by_id(resource_id):
# Map of allowed resources
resource_map = {
"user_profile": "https://api.example.com/profiles/",
"product_data": "https://api.example.com/products/",
"weather_info": "https://api.weather.com/forecast/"
}
# Check if resource_id is in allowed list
if resource_id not in resource_map:
raise ValueError(f"Unknown resource ID: {resource_id}")
# Construct URL from safe base + ID
base_url = resource_map[resource_id]
return requests.get(base_url)
```
6. **Implement Response Handling Controls:**
- Sanitize and validate responses
- Prevent response data from being used in further requests
- Example:
```python
def safe_request_with_response_validation(url):
response = requests.get(url)
# Check response size
if len(response.content) > MAX_RESPONSE_SIZE:
raise ValueError("Response too large")
# Validate content type
content_type = response.headers.get('Content-Type', '')
if not content_type.startswith('application/json'):
raise ValueError(f"Unexpected content type: {content_type}")
# Parse and validate JSON structure
try:
data = response.json()
# Validate expected structure
if 'result' not in data:
raise ValueError("Invalid response structure")
return data
except ValueError:
raise ValueError("Invalid JSON response")
```
7. **Use Timeouts and Circuit Breakers:**
- Set appropriate timeouts
- Implement circuit breakers for failing services
- Example:
```python
import requests
from requests.exceptions import Timeout, ConnectionError
def request_with_circuit_breaker(url, max_retries=3, timeout=5):
retries = 0
while retries < max_retries:
try:
return requests.get(url, timeout=timeout)
except (Timeout, ConnectionError) as e:
retries += 1
if retries >= max_retries:
# Circuit is now open
raise ValueError(f"Circuit breaker open for {url}: {str(e)}")
# Exponential backoff
time.sleep(2 ** retries)
```
8. **Implement Proper Logging and Monitoring:**
- Log all outbound requests
- Monitor for unusual patterns
- Example:
```python
import logging
import requests
def logged_request(url, **kwargs):
# Log the outbound request
logging.info(f"Outbound request to: {url}")
try:
response = requests.get(url, **kwargs)
# Log the response
logging.info(f"Response from {url}: status={response.status_code}")
return response
except Exception as e:
# Log the error
logging.error(f"Request to {url} failed: {str(e)}")
raise
```
9. **Use DNS Resolution Controls:**
- Implement DNS resolution controls
- Block internal DNS names
- Example:
```python
import socket
import ipaddress
def is_safe_host(hostname):
try:
# Resolve hostname to IP
ip_addresses = socket.getaddrinfo(
hostname, None, socket.AF_INET, socket.SOCK_STREAM
)
for family, socktype, proto, canonname, sockaddr in ip_addresses:
ip = sockaddr[0]
ip_obj = ipaddress.ip_address(ip)
# Check if IP is private/internal
if (ip_obj.is_private or ip_obj.is_loopback or
ip_obj.is_link_local or ip_obj.is_reserved):
return False
return True
except (socket.gaierror, ValueError):
return False
def safe_request_with_dns_check(url):
parsed_url = urlparse(url)
hostname = parsed_url.netloc.split(':')[0]
if not is_safe_host(hostname):
raise ValueError(f"Hostname resolves to unsafe IP: {hostname}")
return requests.get(url)
```
10. **Implement Defense in Depth:**
- Combine multiple protection mechanisms
- Don't rely on a single control
- Example comprehensive approach:
```python
class SSRFProtectedClient:
def __init__(self):
self.allowed_domains = ["api.example.com", "cdn.example.com"]
self.allowed_protocols = ["https"]
self.max_redirects = 3
self.timeout = 10
def is_safe_url(self, url):
# URL validation
parsed_url = urlparse(url)
# Protocol check
if parsed_url.scheme not in self.allowed_protocols:
return False
# Domain check
if parsed_url.netloc not in self.allowed_domains:
return False
# DNS resolution check
hostname = parsed_url.netloc.split(':')[0]
try:
ip_addresses = socket.getaddrinfo(
hostname, None, socket.AF_INET, socket.SOCK_STREAM
)
for family, socktype, proto, canonname, sockaddr in ip_addresses:
ip = sockaddr[0]
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_reserved:
return False
except socket.gaierror:
return False
return True
def request(self, method, url, **kwargs):
# Validate URL
if not self.is_safe_url(url):
raise ValueError(f"URL failed security validation: {url}")
# Set sensible defaults
kwargs.setdefault('timeout', self.timeout)
kwargs.setdefault('allow_redirects', False)
# Make initial request
session = requests.Session()
response = session.request(method, url, **kwargs)
# Handle redirects manually with validation
redirect_count = 0
while 300 <= response.status_code < 400 and redirect_count < self.max_redirects:
redirect_url = response.headers.get('Location')
# Validate redirect URL
if not self.is_safe_url(redirect_url):
raise ValueError(f"Redirect URL failed security validation: {redirect_url}")
# Follow the redirect with validation
redirect_count += 1
response = session.request(method, redirect_url, **kwargs)
# Log the request
logging.info(f"{method} request to {url} completed with status {response.status_code}")
return response
def get(self, url, **kwargs):
return self.request('GET', url, **kwargs)
def post(self, url, **kwargs):
return self.request('POST', url, **kwargs)
# Usage
client = SSRFProtectedClient()
response = client.get('https://api.example.com/data')
```
- type: validate
conditions:
# Check 1: URL validation implementation
- pattern: "def\\s+is_valid_url|def\\s+validate_url"
message: "URL validation function is implemented."
# Check 2: Allowlist implementation
- pattern: "allowed_domains|allowed_urls|ALLOWED_HOSTS|whitelist"
message: "URL allowlisting is implemented."
# Check 3: Safe request wrapper
- pattern: "class\\s+\\w+Request|def\\s+safe_request"
message: "Safe request wrapper is implemented."
# Check 4: IP address validation
- pattern: "ipaddress\\.ip_address|is_private|is_loopback|is_reserved"
message: "IP address validation is implemented to prevent access to internal resources."
metadata:
priority: high
version: 1.0
tags:
- security
- python
- ssrf
- owasp
- language:python
- framework:django
- framework:flask
- framework:fastapi
- category:security
- subcategory:ssrf
- standard:owasp-top10
- risk:a10-server-side-request-forgery
references:
- "https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/"
- "https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html"
- "https://portswigger.net/web-security/ssrf"
- "https://docs.python.org/3/library/urllib.request.html"
- "https://docs.python-requests.org/en/latest/user/advanced/#ssl-cert-verification"
- "https://docs.python.org/3/library/ipaddress.html"
</rule>Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
Posts are public.Sign in to post
No one has posted yet. Be the first.

