Summary
The readurls approach fetches every URL it finds in a prompt with no restriction on the destination host, so a caller can steer the proxy into reading internal/link-local addresses (SSRF). This only bites when OptiLLM is exposed to callers you don't fully trust (--host 0.0.0.0, a shared/hosted proxy), but in that setup it's reachable purely from a chat request.
Where
optillm/plugins/readurls_plugin.py — fetch_webpage_content fetches whatever extract_urls pulled out of the prompt text:
# extract_urls: bare regex over the prompt
url_pattern = re.compile(r'https?://[^\s\'"]+')
...
# fetch_webpage_content:41-47
response = requests.get(url, headers=headers, timeout=10, verify=verify)
There's no host resolution and no allow/deny on the target — no block for RFC 1918, loopback, or link-local (169.254.0.0/16, which includes the cloud-metadata IP 169.254.169.254).
Why it's reachable
A caller selects the approach without any server-side opt-in — either via the request body (optillm_approach) or the model slug (readurls-<model>). On a proxy bound to 0.0.0.0, a request like:
messages: [{ role: "user", content: "summarize http://169.254.169.254/latest/meta-data/iam/security-credentials/" }]
model: "readurls-gpt-4o-mini"
makes the server fetch that URL and fold the response into the completion it returns to the caller — i.e. cloud IAM credentials or any internal service reachable from the proxy host.
To be clear about scope: this is not the core forward path (base_url is operator-set, so the main proxy can't be redirected) and it's not the executecode/z3 approaches (those run code by design and are honestly documented as such). It's specifically that readurls is meant to fetch a webpage and, without a destination guard, quietly reaches further than that.
Suggested fix
Resolve the host and refuse private/loopback/link-local ranges before fetching, unless an operator explicitly allows it. Roughly:
import ipaddress, socket
from urllib.parse import urlparse
def _is_blocked_host(url: str) -> bool:
host = urlparse(url).hostname
if not host:
return True
try:
infos = socket.getaddrinfo(host, None)
except socket.gaierror:
return True
for *_, sockaddr in infos:
ip = ipaddress.ip_address(sockaddr[0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return True
return False
and short-circuit in fetch_webpage_content when _is_blocked_host(url) is true (gated behind a config flag such as readurls_allow_internal for anyone who genuinely needs to fetch internal docs). Resolving once and reusing the resolved IP for the actual request would also close the DNS-rebind gap, though the range check is the main thing.
Happy to open a PR if that would help.
Found while reviewing open-source AI/LLM tools; fuller write-up (with what OptiLLM already does well) here.
Summary
The
readurlsapproach fetches every URL it finds in a prompt with no restriction on the destination host, so a caller can steer the proxy into reading internal/link-local addresses (SSRF). This only bites when OptiLLM is exposed to callers you don't fully trust (--host 0.0.0.0, a shared/hosted proxy), but in that setup it's reachable purely from a chat request.Where
optillm/plugins/readurls_plugin.py—fetch_webpage_contentfetches whateverextract_urlspulled out of the prompt text:There's no host resolution and no allow/deny on the target — no block for RFC 1918, loopback, or link-local (
169.254.0.0/16, which includes the cloud-metadata IP169.254.169.254).Why it's reachable
A caller selects the approach without any server-side opt-in — either via the request body (
optillm_approach) or the model slug (readurls-<model>). On a proxy bound to0.0.0.0, a request like:makes the server fetch that URL and fold the response into the completion it returns to the caller — i.e. cloud IAM credentials or any internal service reachable from the proxy host.
To be clear about scope: this is not the core forward path (
base_urlis operator-set, so the main proxy can't be redirected) and it's not theexecutecode/z3approaches (those run code by design and are honestly documented as such). It's specifically thatreadurlsis meant to fetch a webpage and, without a destination guard, quietly reaches further than that.Suggested fix
Resolve the host and refuse private/loopback/link-local ranges before fetching, unless an operator explicitly allows it. Roughly:
and short-circuit in
fetch_webpage_contentwhen_is_blocked_host(url)is true (gated behind a config flag such asreadurls_allow_internalfor anyone who genuinely needs to fetch internal docs). Resolving once and reusing the resolved IP for the actual request would also close the DNS-rebind gap, though the range check is the main thing.Happy to open a PR if that would help.
Found while reviewing open-source AI/LLM tools; fuller write-up (with what OptiLLM already does well) here.