BerriAI/litellm · critical · SSRFError
URL targets a blocked address ({resolved_ip}). If this is a
Error message
URL targets a blocked address ({resolved_ip}). If this is a legitimate internal service, add the host to `user_url_allowed_hosts` in general_settings. What it means
The core SSRF block from litellm's validate_url: after DNS resolution, every resolved IP is checked with _is_blocked_ip, which blocks any non-global IP (RFC 6890: private, loopback, link-local, CGNAT), multicast, unparseable addresses, IPv4-mapped IPv6 forms, and cloud metadata ranges (e.g. Azure Wire Server 168.63.129.16). If any resolved address is blocked, fetching is refused unless the host:port was admin-allowlisted via user_url_allowed_hosts in general_settings. The message deliberately includes the remediation path.
Source
Thrown at litellm/litellm_core_utils/url_utils.py:291
effective_port: Final = port if port is not None else default_port
host_header: Final = _format_host_header(hostname, effective_port, default_port)
is_allowlisted: Final = _is_host_allowlisted(hostname, effective_port)
# Resolve hostname and validate ALL addresses
try:
addrinfo: Final = socket.getaddrinfo(hostname, effective_port, proto=socket.IPPROTO_TCP)
except socket.gaierror as e:
raise SSRFError(f"DNS resolution failed for '{hostname}': {e}")
if not addrinfo:
raise SSRFError(f"No addresses found for '{hostname}'")
if not is_allowlisted:
for family, type_, proto, canonname, sockaddr in addrinfo:
resolved_ip = _sockaddr_host(sockaddr)
if _is_blocked_ip(resolved_ip):
raise SSRFError(
f"URL targets a blocked address ({resolved_ip}). "
"If this is a legitimate internal service, add the host "
"to `user_url_allowed_hosts` in general_settings."
)
# For HTTPS with SSL verification enabled, TLS certificate validation
# binds the connection to the hostname — DNS rebinding can't redirect
# to a different server because the cert wouldn't match.
# When SSL verification is disabled, this defense doesn't apply, so
# we rewrite to the validated IP like HTTP.
ssl_verify: Final = getattr(litellm, "ssl_verify", True)
if parsed.scheme == "https" and ssl_verify is not False:
return url, host_header
# For HTTP, rewrite URL to connect to the validated IP directly
# to prevent DNS rebinding (no TLS to bind the connection).
validated_ip: Final = _sockaddr_host(addrinfo[0][4])
is_ipv6: Final = addrinfo[0][0] == socket.AF_INET6View on GitHub (pinned to 6c2dcb801b)
Solutions
- If the target is a legitimate internal service, ask the proxy admin to add the exact host (optionally host:port, e.g. 'internal-api.corp:8080') to user_url_allowed_hosts under general_settings in the litellm config, then restart the proxy.
- If you did not expect an internal IP, investigate the DNS name — it may be a DNS rebinding attempt or a misconfigured record.
- Point the URL at a globally routable endpoint instead (public hostname or correct external IP).
- Admins: remember allowlisting skips IP checks for that host, so only allowlist hosts you control and trust.
Example fix
# before response = safe_get(client, "http://internal-llm.corp.local:8080/health") # SSRFError: URL targets a blocked address (10.1.2.3)... # after: litellm_config.yaml # general_settings: # user_url_allowed_hosts: # - "internal-llm.corp.local:8080" response = safe_get(client, "http://internal-llm.corp.local:8080/health")
Defensive patterns
Strategy: try-catch
Validate before calling
import socket
from ipaddress import ip_address
def all_ips_global(hostname: str) -> bool:
try:
infos = socket.getaddrinfo(hostname, 443, proto=socket.IPPROTO_TCP)
except socket.gaierror:
return False
ips = [i[4][0] for i in infos]
return all(_is_public(ip) for ip in ips)
def _is_public(addr: str) -> bool:
try:
ip = ip_address(addr)
except ValueError:
return False
return ip.is_global and not ip.is_multicast Try / catch
from litellm.litellm_core_utils.url_utils import SSRFError
try:
resp = safe_get(client, url)
except SSRFError as e:
if "blocked address" in str(e):
return bad_request("target is not reachable under SSRF policy; ask the admin to allowlist it")
raise Prevention
- Do NOT auto-add hosts to user_url_allowed_hosts; that skips IP checks — make it a deliberate admin decision.
- Test locally with globally routable hosts, or run the proxy with an allowlist entry for your dev host.
- Remember every resolved A/AAAA record must be global; one private record blocks the hostname.
- Never point user-supplied URL features at 169.254.169.254 or loopback.
When it happens
Trigger: validate_url/safe_get on a URL whose hostname resolves to a private/internal IP — e.g. http://localhost:8080/fetch, http://169.254.169.254/latest/meta-data (cloud metadata), http://10.0.0.5:9000, or a public-looking DNS name that resolves (round-robin or rebinding) to an internal address. Also triggered when a hostname resolves to multiple addresses and ANY one of them is non-global.
Common situations: Legitimate internal services (self-hosted models, internal file/media hosts) referenced by user-supplied URL fields in the proxy; developers testing locally against 127.0.0.1; multi-A records where one record is private; deployments where the 'user URL' feature is exposed to end users who request internal targets (the actual attack this guards against).
Related errors
- MAVVRIK_API_ENDPOINT host must be a Mavvrik domain (e.g. htt
- Mavvrik FOCUS destination: {label} must be HTTPS, got scheme
- Mavvrik FOCUS destination: {label} must be a GCS endpoint (s
- {field_name} cannot be a dot path segment
- URL scheme '{parsed.scheme}' is not allowed
AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15).
Data as JSON: /api/errors/25ff484475b51dc4.
Report an issue: GitHub.