Skip to content

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/

Additional Resources