CWE-441: Unintended Proxy or Intermediary ('Confused Deputy')
Overview
Unintended proxy vulnerabilities occur when applications forward requests to arbitrary destinations based on user input, acting as open proxies that enable SSRF attacks, bypassing firewalls, port scanning internal networks, and accessing cloud metadata services.
OWASP Classification
A01:2025 - Broken Access Control
Risk
High: Open proxies enable SSRF (access internal services), cloud metadata theft (AWS credentials from 169.254.169.254), firewall bypass, port scanning, DNS rebinding attacks, and proxying attacks through trusted server to access restricted resources.
Remediation Steps
Core principle: Do not let intermediaries become confused deputies; authenticate/authorize the true caller and constrain delegated actions.
Locate the Unintended Proxy/SSRF Vulnerability
When reviewing security scan results:
- Examine data_paths: Identify where user input controls request destinations
- Find proxy endpoints: Look for URL fetching, HTTP forwarding, webhook handlers
- Trace URL parameters: Find where URLs from query strings, headers, or POST data are used
- Check redirect handling: Identify if redirects are followed automatically
- Review DNS resolution: Check if internal hostnames or IPs can be accessed
Validate and Allowlist Destinations (Primary Defense)
ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com']
def fetch_url(url):
parsed = urlparse(url)
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("Destination not allowed")
return requests.get(url)
Why this works: Allowlisting allowed destinations prevents attackers from specifying arbitrary URLs. Only explicitly approved hosts can be accessed, blocking SSRF attempts to internal services, metadata endpoints, or other unintended targets.
Implementation details:
- Maintain an explicit list of allowed hostnames (not patterns)
- Parse URLs properly using URL parsing libraries (urlparse, URL class)
- Validate both hostname and protocol (only allow https:// for external APIs)
- Reject URLs with credentials embedded (http://user:pass@host)
- Don't use regex for URL validation - use proper parsers
Block Internal and Private IP Addresses
import ipaddress
import socket
def is_private_ip(hostname):
try:
ip = ipaddress.ip_address(hostname)
return ip.is_private or ip.is_loopback or ip.is_link_local
except ValueError:
# Resolve hostname and check resulting IP
try:
ip_str = socket.gethostbyname(hostname)
return is_private_ip(ip_str)
except socket.gaierror:
raise ValueError("Invalid hostname")
def fetch_url(url):
parsed = urlparse(url)
if is_private_ip(parsed.hostname):
raise ValueError("Cannot access private/internal IPs")
# Additional checks
return requests.get(url, timeout=5)
Critical IP ranges to block:
- Loopback: 127.0.0.0/8, ::1
- Private networks: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7
- Link-local: 169.254.0.0/16, fe80::/10
- Cloud metadata: 169.254.169.254 (AWS, Azure, GCP)
Use Indirect References Instead of Direct URLs
# Don't accept arbitrary URLs from users
# Bad: GET /proxy?url=http://internal/admin
# Use ID mapping to pre-approved URLs
ALLOWED_URLS = {
'api': 'https://api.example.com/endpoint',
'cdn': 'https://cdn.example.com/assets',
'webhook': 'https://webhook.example.com/hook'
}
def proxy(resource_id):
if resource_id not in ALLOWED_URLS:
abort(400, "Invalid resource ID")
url = ALLOWED_URLS[resource_id]
return requests.get(url, timeout=5)
Benefits of indirect references:
- Users select from predefined options, not arbitrary URLs
- Simplifies validation - only check if ID exists in map
- Prevents URL manipulation attacks completely
- Makes monitoring and auditing easier
Implement Network Segmentation and Security Controls
Network-level defenses:
- Run proxy/fetching services in DMZ with restricted egress
- Block outbound access to metadata services (169.254.169.254) at firewall
- Use egress filtering to allow only necessary external destinations
- Disable HTTP redirects:
requests.get(url, allow_redirects=False) - Set short timeouts to prevent resource exhaustion
- Use DNS with restricted resolution (no internal DNS)
Application-level controls:
# Disable redirects (prevents DNS rebinding)
response = requests.get(url,
allow_redirects=False,
timeout=5,
verify=True # Verify SSL certificates
)
# Limit response size
if int(response.headers.get('Content-Length', 0)) > MAX_SIZE:
raise ValueError("Response too large")
Monitor and Test for SSRF Vulnerabilities
Testing strategies:
- Test with internal IPs:
http://127.0.0.1,http://localhost,http://192.168.1.1 - Test with cloud metadata:
http://169.254.169.254/latest/meta-data/ - Test with DNS rebinding (domain that resolves to internal IP)
- Test with URL encoding:
http://2130706433/(127.0.0.1 in decimal) - Test with IPv6 addresses:
http://[::1]/,http://[::ffff:127.0.0.1]/ - Test redirect chains to internal services
Monitoring:
- Log all outbound HTTP requests with destination URLs
- Alert on requests to private IP ranges
- Monitor for metadata endpoint access attempts
- Track unusual destination patterns
- Set up honeypot internal services to detect SSRF
Verification steps:
- Attempt to access internal services through the proxy
- Try to fetch AWS metadata endpoint
- Test with various URL encoding and bypass techniques
Common Vulnerable Patterns
# Open proxy
@app.route('/fetch')
def fetch():
url = request.args.get('url')
return requests.get(url).text # DANGEROUS!
# SSRF via redirect
def get_data(url):
r = requests.get(url, allow_redirects=True) # Can redirect to internal
return r.text
# Metadata access
# /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/