CVE-2026-17633: Arbitrary code execution through Langflow's custom-component endpointCVE-2026-17633: execução de código arbitrário pelo endpoint de componentes customizados do Langflow
Langflow's custom-component endpoint compiled and ran a submitted component's class body to inspect it, so an authenticated user could execute arbitrary Python in the backend just by defining a malicious component.O endpoint de componentes customizados do Langflow compilava e rodava o corpo da classe de um componente submetido para inspecioná-lo, então um usuário autenticado podia executar Python arbitrário no backend só por definir um componente malicioso.
Disclosure: I reported this to IBM, who ship Langflow OSS, as a coordinated disclosure with their security team; it is fixed in
1.11.0and the CVE is public as of the 2026-08-05 advisory. The finder credit lands in a later revision of the advisory — this writeup goes up ahead of that. If you run a multi-user or hosted Langflow instance you should upgrade, there is no configuration-only workaround.
Summary
CVE-2026-17633 is a high-severity code-execution flaw (CWE-94, Improper
Control of Generation of Code) in Langflow OSS up to and including 1.10.3. It
lets a low-privilege authenticated user run arbitrary Python in the
Langflow backend process through the endpoint that handles custom components,
POST /api/v1/custom_component.
It is a sibling of CVE-2026-17624, published in the same IBM bulletin — arbitrary code execution in component generation, validation, and custom component handling — and it is worth being precise about how they differ. CVE-2026-17624 is about an import allowlist that can be evaded: you reach a forbidden module through a path the validator doesn't inspect. CVE-2026-17633 is the more elementary fact underneath it: handling a custom component means executing it. To tell Langflow what a component's inputs, outputs and template are, the backend compiles the submitted source and runs it — and running a Python class definition runs the code in its body. There is no allowlist to outwit here; the act of describing the component is the act of executing it.
As with the rest of the family, the precondition is not anonymous RCE. The caller has to be a logged-in user of an instance that has custom components enabled. On a single-user local Langflow that is nearly everyone who can reach it; on a shared or hosted multi-tenant deployment it is any tenant, and that is where it bites — a tenant turns "I can author a component in my own flow" into "I can run code as the server."
Affected versions
| Product | Affected | Fixed |
|---|---|---|
| Langflow OSS | 1.0.0 – 1.10.3 | 1.11.0 |
Like its sibling, the CVE carries two CVSS 3.1 scores, and the gap between them is the same gap, for the same reasons:
| Source | Base score | Vector |
|---|---|---|
| IBM (CNA) | 8.5 (High) | CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H |
| NVD (primary) | 8.8 (High) | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
They agree on what matters — C:H/I:H/A:H, full read, write and
availability impact, because arbitrary code is arbitrary code — and on PR:L,
an authenticated attacker. They differ on two axes:
AC:HvsAC:L: IBM treats the attack as higher-complexity — the custom-component path has to be enabled on the instance and you have to be able to submit a component to it. NVD scores the access condition as low, because once that path is reachable, a two-line class is the entire exploit.S:CvsS:U: IBM models a scope change — the code was meant to be confined to a component and instead acts as the host process — which is the honest reading of what this bug does. NVD keeps scope unchanged. That call is most of the 0.3-point difference.
Either way it lands in the high-8s, and either way the fix is the same: 1.11.0.
Background: why handling a custom component means running it
Langflow's whole value proposition is running user-authored logic. A custom
component is a Python class, and before Langflow can draw it on the canvas it
needs to know its shape: its display name, its typed inputs, its outputs, the
template the UI should render. None of that is available from the source text —
it is attached to the class object, which only exists once Python has
executed the class statement that defines it.
So the backend does the natural thing: it compiles the submitted source and executes it, then reads the resulting class object to extract the metadata. That is a reasonable way to introspect a component and a catastrophic way to treat untrusted input, because a class body is ordinary code that runs at definition time. Any statement sitting at module level or in the class body — not inside a method waiting to be called, just there — executes the moment the definition is compiled and run. There is no "build the flow" step to wait for, and no human approval to slip past; submitting the component to the endpoint is enough.
Root cause
The public advisory is terse — "code injection", CWE-94 — so the code below is illustrative, reconstructed from the advisory and Langflow's documented component model, not verbatim source. It shows the class of mistake the CVE names, not a specific patched line.
To turn a submitted component into something it can describe, the handler compiles and runs the source, then inspects the class it produced:
def build_custom_component(code: str) -> ComponentSpec:
# Langflow needs the actual class object to read its inputs, outputs and
# template — metadata that only exists once the `class` statement has run.
namespace: dict = {}
# Compiling and executing the source runs every top-level and class-body
# statement in it, right now, in this process.
exec(compile(code, "<custom_component>", "exec"), namespace)
cls = _find_component_class(namespace)
return ComponentSpec(display_name=cls.display_name, inputs=cls.inputs)The single load-bearing line is the exec. Everything else is ordinary
introspection; the mistake is that it is performed by running attacker source.
And because the goal is to obtain a class object, there is no intermediate state
to attack — the code the attacker wants to run does not sit in a method that
Langflow might or might not call later. It sits in the class body, and defining
the class is running it:
import os
class TotallyNormal(CustomComponent):
display_name = "Totally Normal"
# Not a method. A class-body statement — it executes the instant the class
# is defined, i.e. the instant `exec()` compiles the source above. No flow
# run, no method call, no human approval.
os.system("id > /tmp/pwned")The fix in 1.11.0 is the same move the whole bulletin converges on: stop
treating "run it and look at the result" as a way to inspect untrusted code.
The generic lesson is the one CWE-94 keeps teaching — you cannot safely learn
what Python code is by executing it. Introspection that needs a live class
object has already handed the author a shell; the only sound place to draw the
line is before the code runs at all, not after.
Exploitation
The clean threat model is any Langflow OSS deployment where custom components are enabled and the attacker can log in:
- The attacker is a registered, authenticated user (the
PR:Lprecondition). On a shared or hosted multi-tenant instance, that's any tenant. - The instance accepts custom components — the documented
LANGFLOW_ALLOW_CUSTOM_COMPONENTSpath — which is what makes the endpoint reachable (and is part of why IBM scoresAC:H).
From there the shape is as blunt as the root cause: submit a component whose class body runs a command, and let the endpoint's own introspection run it.
POST /api/v1/custom_component HTTP/1.1
Content-Type: application/json
Cookie: <attacker's authenticated session>
{ "code": "<component whose class body runs an OS command>" }What follows is C:H/I:H/A:H: the payload runs as the Langflow backend process,
which means read and write of anything that process can touch — other tenants'
flows and data, stored provider credentials and API keys, the environment, the
filesystem — and, from there, whatever the host allows. The scope-change reading
in IBM's vector is the point: the code was supposed to be confined to a
component, and it isn't.
The components above are illustrative reconstructions built from the public advisory and Langflow's documented component model, not a capture against a live instance. Only run them against a deployment you own or are explicitly authorized to assess.
Timeline
- 2026-08-05: CVE-2026-17633 published; the IBM bulletin covering Langflow OSS component handling is posted.
- 2026-08-24: IBM bulletin last modified.
1.11.0: Langflow OSS ships with custom-component handling fixed; this is the first fixed release.- (pending): finder credit added in a later revision of the IBM advisory.
- 2026-10-19: this writeup.
References
Divulgação: reportei isto à IBM, que distribui o Langflow OSS, como uma divulgação coordenada com o time de segurança deles; está corrigido na
1.11.0e a CVE é pública desde o advisory de 05/08/2026. O crédito de quem encontrou entra numa revisão posterior do advisory — este writeup sai antes disso. Se você roda uma instância multiusuário ou hospedada do Langflow, atualize: não existe workaround apenas por configuração.
Resumo
A CVE-2026-17633 é uma falha de execução de código de severidade alta (CWE-94,
Improper Control of Generation of Code) no Langflow OSS até a 1.10.3 inclusive.
Ela permite que um usuário autenticado de baixo privilégio rode Python
arbitrário no processo de backend do Langflow pelo endpoint que trata
componentes customizados, POST /api/v1/custom_component.
É irmã da CVE-2026-17624, publicada no mesmo boletim da IBM — execução de código arbitrário em geração, validação e tratamento de componentes customizados — e vale ser preciso sobre como as duas diferem. A CVE-2026-17624 é sobre uma allowlist de imports que pode ser driblada: você alcança um módulo vetado por um caminho que o validador não inspeciona. A CVE-2026-17633 é o fato mais elementar embaixo dela: tratar um componente customizado significa executá-lo. Para dizer ao Langflow quais são as entradas, saídas e o template de um componente, o backend compila o fonte submetido e o roda — e rodar a definição de uma classe Python roda o código que está no corpo dela. Aqui não há allowlist para enganar; o ato de descrever o componente é o ato de executá-lo.
Como no resto da família, a precondição não é RCE anônimo. O chamador precisa ser um usuário logado de uma instância que tenha componentes customizados habilitados. Num Langflow local de usuário único isso é quase todo mundo que consegue alcançá-lo; numa implantação compartilhada ou hospedada multi-inquilino é qualquer inquilino, e é aí que dói — um inquilino transforma "posso criar um componente no meu próprio flow" em "posso rodar código como o servidor".
Versões afetadas
| Produto | Afetadas | Corrigido |
|---|---|---|
| Langflow OSS | 1.0.0 – 1.10.3 | 1.11.0 |
Como a irmã, a CVE carrega duas pontuações CVSS 3.1, e a distância entre elas é a mesma distância, pelos mesmos motivos:
| Fonte | Pontuação base | Vetor |
|---|---|---|
| IBM (CNA) | 8.5 (Alta) | CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H |
| NVD (primária) | 8.8 (Alta) | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
Elas concordam no que importa — C:H/I:H/A:H, impacto total de leitura,
escrita e disponibilidade, porque código arbitrário é código arbitrário — e em
PR:L, um atacante autenticado. Divergem em dois eixos:
AC:HvsAC:L: a IBM trata o ataque como de complexidade mais alta — o caminho de componente customizado precisa estar habilitado na instância e você precisa conseguir submeter um componente a ele. A NVD pontua a condição de acesso como baixa, porque, uma vez que esse caminho esteja alcançável, uma classe de duas linhas é o exploit inteiro.S:CvsS:U: a IBM modela uma mudança de escopo — o código devia estar confinado a um componente e em vez disso age como o processo do host —, que é a leitura honesta do que este bug faz. A NVD mantém o escopo inalterado. Essa decisão responde por quase toda a diferença de 0.3 ponto.
De todo jeito fica nos 8 e pouco, e de todo jeito a correção é a mesma: 1.11.0.
Contexto: por que tratar um componente customizado significa rodá-lo
A proposta de valor inteira do Langflow é rodar lógica escrita pelo usuário. Um
componente customizado é uma classe Python, e antes de o Langflow desenhá-lo no
canvas ele precisa saber a forma dele: o display name, as entradas tipadas, as
saídas, o template que a UI deve renderizar. Nada disso está disponível a partir
do texto do fonte — está preso ao objeto de classe, que só existe depois que
o Python executou a instrução class que o define.
Então o backend faz o óbvio: compila o fonte submetido e o executa, depois lê o objeto de classe resultante para extrair os metadados. Esse é um jeito razoável de inspecionar um componente e um jeito catastrófico de tratar entrada não confiável, porque o corpo de uma classe é código comum que roda no momento da definição. Qualquer instrução que esteja no nível do módulo ou no corpo da classe — não dentro de um método esperando ser chamado, só ali — executa no instante em que a definição é compilada e rodada. Não há etapa de "construir o flow" para esperar, nem aprovação humana para driblar; submeter o componente ao endpoint já basta.
Causa raiz
O advisory público é lacônico — "code injection", CWE-94 — então o código abaixo é ilustrativo, reconstruído a partir do advisory e do modelo de componentes documentado do Langflow, não é o fonte literal. Ele mostra a classe de erro que a CVE nomeia, não uma linha específica corrigida.
Para transformar um componente submetido em algo que consegue descrever, o handler compila e roda o fonte, depois inspeciona a classe que ele produziu:
def build_custom_component(code: str) -> ComponentSpec:
# O Langflow precisa do objeto de classe de fato para ler suas entradas,
# saídas e template — metadados que só existem depois que a instrução `class`
# rodou.
namespace: dict = {}
# Compilar e executar o fonte roda toda instrução de nível superior e de corpo
# de classe que há nele, agora, neste processo.
exec(compile(code, "<custom_component>", "exec"), namespace)
cls = _find_component_class(namespace)
return ComponentSpec(display_name=cls.display_name, inputs=cls.inputs)A única linha que sustenta tudo é o exec. Todo o resto é introspecção comum; o
erro é que ela é feita rodando o fonte do atacante. E como o objetivo é obter um
objeto de classe, não há estado intermediário para atacar — o código que o atacante
quer rodar não fica num método que o Langflow talvez chame depois. Ele fica no corpo
da classe, e definir a classe é rodá-lo:
import os
class TotallyNormal(CustomComponent):
display_name = "Totally Normal"
# Não é um método. É uma instrução de corpo de classe — executa no instante em
# que a classe é definida, isto é, no instante em que o `exec()` compila o fonte
# acima. Sem rodar flow, sem chamar método, sem aprovação humana.
os.system("id > /tmp/pwned")A correção na 1.11.0 é o mesmo movimento para o qual o boletim inteiro converge:
parar de tratar "roda e olha o resultado" como um jeito de inspecionar código não
confiável. A lição genérica é a que a CWE-94 não cansa de ensinar — não dá para
descobrir com segurança o que um código Python é executando-o. Uma introspecção
que precisa de um objeto de classe vivo já entregou um shell ao autor; o único lugar
sólido para traçar a linha é antes de o código rodar, não depois.
Exploração
O modelo de ameaça limpo é qualquer implantação do Langflow OSS onde componentes customizados estejam habilitados e o atacante consiga logar:
- O atacante é um usuário registrado e autenticado (a precondição
PR:L). Numa instância compartilhada ou hospedada multi-inquilino, isso é qualquer inquilino. - A instância aceita componentes customizados — o caminho documentado
LANGFLOW_ALLOW_CUSTOM_COMPONENTS—, que é o que torna o endpoint alcançável (e é parte de por que a IBM pontuaAC:H).
A partir daí a forma é tão direta quanto a causa raiz: submeter um componente cujo corpo de classe roda um comando, e deixar a própria introspecção do endpoint rodá-lo.
POST /api/v1/custom_component HTTP/1.1
Content-Type: application/json
Cookie: <sessão autenticada do atacante>
{ "code": "<componente cujo corpo de classe roda um comando de SO>" }O que se segue é C:H/I:H/A:H: o payload roda como o processo de backend do
Langflow, o que significa leitura e escrita de qualquer coisa que esse processo
alcance — flows e dados de outros inquilinos, credenciais de provedores e chaves de
API armazenadas, ambiente, sistema de arquivos — e, a partir daí, o que o host
permitir. A leitura de mudança de escopo no vetor da IBM é o ponto: o código devia
estar confinado a um componente, e não está.
Os componentes acima são reconstruções ilustrativas montadas a partir do advisory público e do modelo de componentes documentado do Langflow, não uma captura contra uma instância viva. Só os rode contra uma implantação que você possua ou esteja explicitamente autorizado a avaliar.
Linha do tempo
- 05/08/2026: CVE-2026-17633 publicada; o boletim da IBM que cobre o tratamento de componentes do Langflow OSS é postado.
- 24/08/2026: última modificação do boletim da IBM.
1.11.0: o Langflow OSS sai com o tratamento de componentes customizados corrigido; é a primeira release corrigida.- (pendente): crédito de quem encontrou adicionado numa revisão posterior do advisory da IBM.
- 19/10/2026: este writeup.