Post

The Model Will Fetch Anything: SSRF in LMDeploy

You give a vision model an image by URL and the server fetches it for you. LMDeploy fetched any URL at all, including the cloud metadata endpoint, with no authentication required. CVE-2026-33626 was hit in the wild about twelve hours after the advisory went public.

The Model Will Fetch Anything: SSRF in LMDeploy

CVE-2026-33626high 7.5

Here is the whole bug in one breath: to send a picture to a multimodal model, you usually hand the server a URL and it downloads the image for you. LMDeploy downloaded whatever URL you gave it, with no checks, and by default it listened on the open internet with no login. Point it at the cloud metadata service and the server fetches your cloud credentials on your behalf.

The reason this one is worth remembering is the clock. The advisory went public, and roughly twelve and a half hours later the first real attacker was already in a honeypot running the exact attack. Not a week. Not a proof of concept from a researcher. Half a day to live exploitation by someone who does this for money.

Let me take it from zero.

What LMDeploy is and where it sits

Large language models do not serve themselves. Between the trained weights and an application there is an inference server: the thing that loads the model onto the GPU and answers requests over an API. LMDeploy, from the InternLM project, is one of these. It compresses, deploys, and serves models, and it speaks the same API shape as OpenAI’s, so you talk to it by POSTing to /v1/chat/completions.

Plenty of these models are vision-language models, also called multimodal. They take images as well as text. You can ask one “what is in this picture?” and it will tell you. The question is how the picture gets to the server.

The common convention, again borrowed from OpenAI’s API, is that you do not upload the raw image bytes. You send a URL, and the server goes and fetches the image itself:

1
2
3
4
5
6
7
8
9
10
{
  "model": "...",
  "messages": [{
    "role": "user",
    "content": [
      {"type": "text", "text": "describe this image"},
      {"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg"}}
    ]
  }]
}

That image_url is the entire attack surface. The moment a server fetches a URL that a user controls, you have to ask: fetch it from where, with whose network access, and with what restrictions?

SSRF, framed for this case

Server-side request forgery is getting a server to make a request you chose, so you can borrow the server’s position on the network. In a web app the dangerous version reaches internal services or the cloud metadata endpoint. In an AI serving stack the shape is the same, the door is just labeled “image loader” instead of “URL preview” or “webhook.”

The specific target worth naming is the cloud metadata service at 169.254.169.254. Every major cloud exposes it to its VMs, unauthenticated, and it will hand out temporary credentials for the identity the VM runs as. It is safe only as long as nothing lets an outsider make requests from the VM. An image loader that fetches arbitrary URLs is exactly such a thing.

The vulnerable code

The flaw lived in lmdeploy/vl/utils.py, in the load_image() helper. If the image reference looked like a URL, it fetched it. That was the whole check:

1
2
3
4
5
6
# lmdeploy/vl/utils.py  (simplified)
def load_image(image_url):
    if image_url.startswith('http'):
        response = requests.get(image_url, headers=headers, timeout=FETCH_TIMEOUT)
        # no validation of the host or resolved IP before the request
        ...

Three things stacked up to make it exploitable:

  1. No validation before fetching. Any string starting with http was fetched, full stop.
  2. No blocklist for internal addresses. Nothing stopped 127.0.0.1, the 169.254.x.x link-local range, or the RFC 1918 private ranges (10.x, 192.168.x, and friends).
  3. No authentication, on an open bind. The server listened on 0.0.0.0 by default, so it answered requests from anywhere, and it did not require a login to use the chat endpoint.

Put together, an unauthenticated request from the internet could steer an internal fetch. That is why the CVSS vector reads PR:N, no privileges required. You did not need an account. You needed the server’s address.

The attack, which is almost insultingly simple

There is no clever chain here. You send a normal-looking chat request and put an internal URL where the image should be:

1
2
3
4
5
6
7
8
9
10
11
{
  "model": "whatever-is-loaded",
  "messages": [{
    "role": "user",
    "content": [
      {"type": "text", "text": "hi"},
      {"type": "image_url",
       "image_url": {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}}
    ]
  }]
}

The server tries to “load the image.” It fetches the metadata path instead, and from there the attacker walks the credential path. Swap the URL and the same request probes internal services, maps the private network, or pokes at anything else the box can reach.

What the real attackers actually did

This is the part that stops it being academic. The advisory became public on April 21, 2026. About twelve hours and thirty-one minutes later, a Sysdig honeypot running LMDeploy caught the first live exploitation attempt, and the session was a tidy little tour of everything SSRF is good for. Over roughly eight minutes the attacker:

  • port-scanned the internal network by pointing the image loader at internal hosts and ports and watching how the responses differed,
  • hit the AWS metadata service (169.254.169.254) for instance credentials,
  • probed Redis and MySQL on their default ports,
  • poked a secondary admin HTTP interface that was never meant to face anyone,
  • and set up out-of-band DNS exfiltration to carry data out even where the HTTP response was not directly readable.

That is a complete internal-recon-and-pivot playbook, automated, running within half a day of disclosure. The lesson I take from it is not subtle: AI serving infrastructure is now firmly in the set of things that get scanned and exploited at internet speed, the same as any other web service. Treating a model server as “just an internal tool” is how it ends up on 0.0.0.0 with no auth.

The fix, and what to do

The fix shipped in 0.12.3 (PR #4447). Upgrade off anything <= 0.12.2.

Beyond upgrading, the defenses are the ordinary SSRF hygiene that this class always wants, and they are worth applying to any inference server you run, not just this one:

  • Do not bind inference servers to 0.0.0.0 on untrusted networks. Put them behind authentication and a reverse proxy.
  • If the server fetches user-supplied URLs, it must validate the resolved IP against a blocklist of loopback, link-local (169.254.0.0/16), and private ranges, after DNS resolution, not just the hostname string.
  • On the cloud side, enforce IMDSv2 on AWS and the equivalent hardening elsewhere, so a blind GET cannot walk away with credentials.
  • Block egress from the serving pod or VM to the metadata address and to internal services it has no business reaching.

Timeline

  • Apr 18, 2026: Advisory published (GHSA-6w67-hwm5-92mq).
  • Apr 21, 2026: Advisory reviewed and public; CVE-2026-33626 live.
  • Apr 21, 2026, about 12.5 hours later: First exploitation attempt observed against a honeypot.

Details and credits

  • Component: LMDeploy (InternLM toolkit for compressing, deploying, and serving LLMs, including vision-language models)
  • Class: Server-side request forgery (CWE-918)
  • CVSS: 7.5 High, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
  • Affected: <= 0.12.2
  • Fixed: 0.12.3 (PR #4447)
  • Reported by: Igor Stepansky, Orca Security Research Pod, co-reported with kexinoh

References

This post is licensed under CC BY 4.0 by the author.