You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
readurls approach fetches attacker-named URLs with no SSRF guard (metadata/internal reachable on hosted proxy) #323
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 prompturl_pattern=re.compile(r'https?://[^\s\'"]+')
...
# fetch_webpage_content:41-47response=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:
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:
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.
The problem is in optillm/plugins/readurls_plugin.py. The fetch_webpage_content function fetches URLs without checking if they are internal or unsafe. This can lead to SSRF vulnerabilities. I would first inspect the extract_urls and fetch_webpage_content functions. Then, I would add a check to block private, loopback, and link-local IP ranges before making a request. This should prevent unauthorized access to internal resources.
I am working on this issue. I will add a check to block private, loopback, and link-local IP ranges in the fetch_webpage_content function. This should help prevent unauthorized access to internal resources. Let me know if there are any specific requirements or suggestions.
Hi @elfrost — I'd like to pick this up. I noticed @modelpath-dev claimed it in September but no PR has landed after ~4 weeks, and PR #326 (which took the same approach) was closed unmerged by its author. To avoid another stall I'll submit a lean version.
Root cause: optillm/plugins/readurls_plugin.py:47 — fetch_webpage_content calls requests.get() on any URL extracted from the prompt with no restriction on the destination host (no RFC1918/loopback/link-local check, so cloud metadata 169.254.169.254 is reachable).
Plan: pure-stdlib guard (ipaddress + socket.getaddrinfo) that refuses non-public/reserved hosts before fetching, fail-closed on DNS errors, plus redirect re-validation (each hop is re-checked, so a public URL can't 302 into an internal address). Default-deny with an explicit opt-out flag for operators who need internal fetches. Offline unit tests following the existing tests/test_ssl_config.py mock pattern. PR incoming shortly.
Activity
modelpath-dev commentedon Sep 10, 2026
I will take this issue. Please assign it to me.
The problem is in
optillm/plugins/readurls_plugin.py. Thefetch_webpage_contentfunction fetches URLs without checking if they are internal or unsafe. This can lead to SSRF vulnerabilities. I would first inspect theextract_urlsandfetch_webpage_contentfunctions. Then, I would add a check to block private, loopback, and link-local IP ranges before making a request. This should prevent unauthorized access to internal resources.modelpath-dev commentedon Sep 13, 2026
I am working on this issue. I will add a check to block private, loopback, and link-local IP ranges in the
fetch_webpage_contentfunction. This should help prevent unauthorized access to internal resources. Let me know if there are any specific requirements or suggestions.akushonkamen commentedon Oct 10, 2026
Hi @elfrost — I'd like to pick this up. I noticed @modelpath-dev claimed it in September but no PR has landed after ~4 weeks, and PR #326 (which took the same approach) was closed unmerged by its author. To avoid another stall I'll submit a lean version.
Root cause:
optillm/plugins/readurls_plugin.py:47—fetch_webpage_contentcallsrequests.get()on any URL extracted from the prompt with no restriction on the destination host (no RFC1918/loopback/link-local check, so cloud metadata 169.254.169.254 is reachable).Plan: pure-stdlib guard (
ipaddress+socket.getaddrinfo) that refuses non-public/reserved hosts before fetching, fail-closed on DNS errors, plus redirect re-validation (each hop is re-checked, so a public URL can't 302 into an internal address). Default-deny with an explicit opt-out flag for operators who need internal fetches. Offline unit tests following the existingtests/test_ssl_config.pymock pattern. PR incoming shortly.fix(readurls): block SSRF destinations via stdlib host guard