joaovicdev
postartigoCVE-2026-17633

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.0 and 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

ProductAffectedFixed
Langflow OSS1.0.0 – 1.10.31.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:

SourceBase scoreVector
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:H vs AC: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:C vs S: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:

custom component handling (illustrative — reconstructed, not verbatim source)
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:

malicious custom component (illustrative)
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:L precondition). On a shared or hosted multi-tenant instance, that's any tenant.
  • The instance accepts custom components — the documented LANGFLOW_ALLOW_CUSTOM_COMPONENTS path — which is what makes the endpoint reachable (and is part of why IBM scores AC: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.

the request (illustrative)
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

  1. 2026-08-05: CVE-2026-17633 published; the IBM bulletin covering Langflow OSS component handling is posted.
  2. 2026-08-24: IBM bulletin last modified.
  3. 1.11.0: Langflow OSS ships with custom-component handling fixed; this is the first fixed release.
  4. (pending): finder credit added in a later revision of the IBM advisory.
  5. 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.0 e 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

ProdutoAfetadasCorrigido
Langflow OSS1.0.0 – 1.10.31.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:

FontePontuação baseVetor
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:H vs AC: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:C vs S: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:

tratamento de componente customizado (ilustrativo — reconstruído, não é o fonte literal)
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:

componente customizado malicioso (ilustrativo)
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 pontua AC: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.

a requisição (ilustrativo)
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

  1. 05/08/2026: CVE-2026-17633 publicada; o boletim da IBM que cobre o tratamento de componentes do Langflow OSS é postado.
  2. 24/08/2026: última modificação do boletim da IBM.
  3. 1.11.0: o Langflow OSS sai com o tratamento de componentes customizados corrigido; é a primeira release corrigida.
  4. (pendente): crédito de quem encontrou adicionado numa revisão posterior do advisory da IBM.
  5. 19/10/2026: este writeup.

Referências