joaovicdev
postartigoCVE-2026-17627

CVE-2026-17627: Reaching another user's flow through Langflow's voice-mode WebSocketCVE-2026-17627: alcançando o flow de outro usuário pelo WebSocket de voz do Langflow

Langflow's voice-mode WebSocket authenticated the caller but never checked they owned the flow named in the URL, so any user could read another tenant's flow and inject messages into its history.O WebSocket de voz do Langflow autenticava o chamador, mas nunca verificava se ele era dono do flow informado na URL — então qualquer usuário podia ler o flow de outro e injetar mensagens no histórico dele.

Disclosure: I reported this to IBM, who ship Langflow OSS; it is fixed in 1.10.3 and public as of the 2026-09-03 advisory. If you run a multi-user Langflow instance you should upgrade — there is no configuration-only workaround — and I'm credited as the reporter (joaovicdev).

IBM portal print of CVE 2026 17627 with credits joaovicdev

Summary

CVE-2026-17627 is a medium authorization flaw (CWE-639, Authorization Bypass Through User-Controlled Key) in Langflow OSS up to and including 1.10.2. It lets a low-privilege authenticated user on a shared, multi-user instance reach a flow that belongs to someone else: read that flow's description, and inject messages into its conversation history.

The vulnerable surface is the voice-mode WebSocket endpoint, which takes the target flow's identifier as a path parameter in the URL. The endpoint does two checks and skips the one that matters:

  • it confirms the caller is authenticated, and
  • it confirms the caller has a personal OpenAI API key configured,

but it never confirms the caller is authorized to touch the flow they named. That missing ownership check is the whole bug: the flow id in the URL is a user-controlled key, and the handler trusts it instead of scoping the lookup to the caller.

It is worth saying plainly what this is not. It is not RCE, not a flow takeover, and not a way to run someone else's flow — Langflow's execution path applies its own, separate authorization check, so that boundary holds. The damage is confined to reading a flow's description and writing into its message history.

Affected versions

ProductAffectedFixedCVSS 3.1
Langflow OSS1.0.0 – 1.10.21.10.34.9 (Medium)

Affected on Windows, Linux and macOS alike — it's a logic flaw in the handler, not anything platform-specific.

Official vector (IBM):

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N

A few parts of that vector explain why a cross-tenant access bug lands at 4.9 rather than higher:

  • AV:N / PR:L: reachable over the network, but only by an authenticated user — a tenant on the instance, not an anonymous visitor.
  • AC:H: the attacker has to know the victim's flow id. Flow ids are UUIDs, so they aren't guessable at scale; you need to have seen or obtained one (a shared link, a screenshot, a log, a chatty error).
  • S:C: scope is changed — the action crosses from the attacker's authorization context into another user's, which is exactly the property that makes an IDOR an IDOR.
  • C:L / I:L / A:N: limited read (a flow's description) and limited write (messages appended to its history); nothing is destroyed and nothing becomes unavailable.

Root cause

The endpoint is a WebSocket route keyed by the flow id. Stripped to the part that matters, the vulnerable shape fetches the flow by id alone, after gating on authentication and the caller's own API key:

voice_mode.py (illustrative — reconstructed from the advisory, not verbatim source)
@router.websocket("/ws/{flow_id}")
async def voice_mode_websocket(
    websocket: WebSocket,
    flow_id: str,
    current_user: User = Depends(get_current_user_ws),
):
    # Gate 1: the caller is a real, logged-in user.
    # Gate 2: the caller has a personal OpenAI key configured.
    if not current_user.openai_api_key:
        await websocket.close(code=1008)
        return
 
    # The flow is then looked up by id ALONE — no owner scoping.
    flow = await get_flow_by_id(flow_id)        # missing: AND user_id == current_user.id
    await websocket.accept()
    await websocket.send_json({"description": flow.description})
    # ...and messages the caller sends get appended to THIS flow's history.

The two gates it does run are real access controls — they just answer the wrong question. "Is this a valid user?" and "can this user talk to OpenAI?" are both true for the attacker. The question nobody asked is "does this flow belong to this user?", and because the flow id arrives straight from the URL, the answer was simply assumed to be yes.

The fix scopes the lookup to the authenticated caller in the query itself, so an id that isn't yours comes back empty and the connection is refused — rather than fetching the row first and (not) checking ownership after:

voice_mode.py (fixed in 1.10.3 — scope the lookup by the caller)
    # The flow lookup is constrained to the caller's own flows.
    flow = await get_flow_by_id(flow_id, user_id=current_user.id)
    if flow is None:
        await websocket.close(code=1008)   # not yours (or doesn't exist) → refuse
        return

This is the generic lesson behind CWE-639: when a handler takes an id from the caller, the authorization has to live in the same breath as the lookup. Fetch first and check later and you get this bug; forget to check at all and you get exactly this bug.

Exploitation

The clean threat model is a shared Langflow OSS deployment with more than one user — a team instance, or a hosted multi-tenant setup:

  • The attacker is a registered, logged-in user with their own OpenAI API key configured (both gates the endpoint actually enforces).
  • The attacker has gotten hold of a victim's flow id — the AC:H precondition.

With that, the attacker opens a voice-mode WebSocket pointed at the victim's flow instead of one of their own:

the request (illustrative)
GET /api/v1/voice/ws/<VICTIM_FLOW_ID> HTTP/1.1
Upgrade: websocket
Cookie: <attacker's authenticated session>

Two things follow, matching C:L and I:L in the vector:

  1. Read. The server hands back the target flow's description — content the attacker was never meant to see.
  2. Write. Messages the attacker sends over the socket are appended to the victim flow's conversation history. The flow's owner then sees those injected messages later through their own, owner-scoped message-monitor API — so this is also a way to put words into a conversation the owner trusts as theirs.

The boundary that holds is execution: the attacker cannot actually run the victim's flow through this path, because Langflow authorizes execution separately. The bug is a read-and-inject primitive across the user boundary, not a hijack of the flow.

This description is built from the public advisory and the documented endpoint behaviour, not from a capture against a live instance. Only test it against a deployment you own or are explicitly authorized to assess.

Timeline

  1. 2026-09-03: IBM publishes the advisory for CVE-2026-17627.
  2. 1.10.3: Langflow OSS ships with the ownership check in place; this is the first fixed release.
  3. 2026-10-16: advisory last modified.
  4. 2026-10-19: this writeup.

References

Divulgação: reportei isto à IBM, que distribui o Langflow OSS; está corrigido na 1.10.3 e é público desde o advisory de 03/09/2026. Se você roda uma instância multiusuário do Langflow, atualize — não existe workaround apenas por configuração — e sou creditado como o reporter (joaovicdev).

IBM portal print of CVE 2026 17627 with credits joaovicdev

Resumo

A CVE-2026-17627 é uma falha média de autorização (CWE-639, Authorization Bypass Through User-Controlled Key) no Langflow OSS até a 1.10.2 inclusive. Ela permite que um usuário autenticado de baixo privilégio numa instância compartilhada e multiusuário alcance um flow que pertence a outra pessoa: leia a descrição desse flow e injete mensagens no histórico de conversa dele.

A superfície vulnerável é o endpoint WebSocket do modo de voz, que recebe o identificador do flow alvo como parâmetro de caminho na URL. O endpoint faz duas verificações e pula justamente a que importa:

  • confirma que o chamador está autenticado, e
  • confirma que o chamador tem uma chave pessoal da API da OpenAI configurada,

mas nunca confirma que o chamador está autorizado a mexer no flow que ele nomeou. Essa verificação de propriedade ausente é o bug inteiro: o id do flow na URL é uma chave controlada pelo usuário, e o handler confia nela em vez de restringir a busca ao chamador.

Vale dizer com clareza o que isto não é. Não é RCE, não é tomada de controle do flow, e não é um jeito de rodar o flow de outra pessoa — o caminho de execução do Langflow aplica a própria verificação de autorização, separada, então essa fronteira se mantém. O dano se limita a ler a descrição de um flow e escrever no histórico de mensagens dele.

Versões afetadas

ProdutoAfetadasCorrigidoCVSS 3.1
Langflow OSS1.0.0 – 1.10.21.10.34.9 (Média)

Afetado no Windows, Linux e macOS igualmente — é uma falha de lógica no handler, não algo específico de plataforma.

Vetor oficial (IBM):

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:N

Algumas partes desse vetor explicam por que um bug de acesso entre inquilinos tira 4.9 e não mais:

  • AV:N / PR:L: alcançável pela rede, mas só por um usuário autenticado — um inquilino da instância, não um visitante anônimo.
  • AC:H: o atacante precisa conhecer o id do flow da vítima. Ids de flow são UUIDs, então não dá para adivinhá-los em escala; é preciso ter visto ou obtido um (um link compartilhado, uma captura de tela, um log, um erro tagarela).
  • S:C: o escopo é alterado — a ação cruza do contexto de autorização do atacante para o de outro usuário, que é exatamente a propriedade que faz de um IDOR um IDOR.
  • C:L / I:L / A:N: leitura limitada (a descrição de um flow) e escrita limitada (mensagens anexadas ao histórico dele); nada é destruído e nada fica indisponível.

Causa raiz

O endpoint é uma rota WebSocket chaveada pelo id do flow. Reduzido à parte que importa, o padrão vulnerável busca o flow só pelo id, depois de barrar na autenticação e na chave de API do próprio chamador:

voice_mode.py (ilustrativo — reconstruído a partir do advisory, não é o código-fonte literal)
@router.websocket("/ws/{flow_id}")
async def voice_mode_websocket(
    websocket: WebSocket,
    flow_id: str,
    current_user: User = Depends(get_current_user_ws),
):
    # Barreira 1: o chamador é um usuário real, logado.
    # Barreira 2: o chamador tem uma chave pessoal da OpenAI configurada.
    if not current_user.openai_api_key:
        await websocket.close(code=1008)
        return
 
    # O flow é então buscado SÓ pelo id — sem restringir ao dono.
    flow = await get_flow_by_id(flow_id)        # faltando: AND user_id == current_user.id
    await websocket.accept()
    await websocket.send_json({"description": flow.description})
    # ...e as mensagens que o chamador envia são anexadas ao histórico DESTE flow.

As duas barreiras que ele de fato executa são controles de acesso reais — elas só respondem à pergunta errada. "Isto é um usuário válido?" e "esse usuário consegue falar com a OpenAI?" são ambas verdadeiras para o atacante. A pergunta que ninguém fez é "este flow pertence a este usuário?", e como o id do flow chega direto da URL, a resposta foi simplesmente assumida como sim.

A correção restringe a busca ao chamador autenticado na própria query, de modo que um id que não é seu volta vazio e a conexão é recusada — em vez de buscar a linha primeiro e (não) verificar a propriedade depois:

voice_mode.py (corrigido na 1.10.3 — restringir a busca ao chamador)
    # A busca do flow fica limitada aos flows do próprio chamador.
    flow = await get_flow_by_id(flow_id, user_id=current_user.id)
    if flow is None:
        await websocket.close(code=1008)   # não é seu (ou não existe) → recusa
        return

Esta é a lição genérica por trás da CWE-639: quando um handler recebe um id do chamador, a autorização tem que viver no mesmo fôlego que a busca. Busque primeiro e verifique depois e você tem este bug; esqueça de verificar e você tem exatamente este bug.

Exploração

O modelo de ameaça limpo é uma instalação compartilhada do Langflow OSS com mais de um usuário — uma instância de time, ou um setup multi-inquilino hospedado:

  • O atacante é um usuário registrado e logado com a própria chave da API da OpenAI configurada (as duas barreiras que o endpoint realmente aplica).
  • O atacante conseguiu o id de um flow da vítima — a pré-condição AC:H.

Com isso, o atacante abre um WebSocket do modo de voz apontado para o flow da vítima em vez de um dos seus:

a requisição (ilustrativa)
GET /api/v1/voice/ws/<VICTIM_FLOW_ID> HTTP/1.1
Upgrade: websocket
Cookie: <sessão autenticada do atacante>

Duas coisas acontecem, casando com C:L e I:L no vetor:

  1. Leitura. O servidor devolve a descrição do flow alvo — conteúdo que o atacante nunca deveria ver.
  2. Escrita. As mensagens que o atacante envia pelo socket são anexadas ao histórico de conversa do flow da vítima. O dono do flow então vê essas mensagens injetadas mais tarde, pela própria API de monitoramento de mensagens (com escopo de dono) — então isto também é um jeito de colocar palavras numa conversa que o dono confia ser dele.

A fronteira que se mantém é a execução: o atacante não consegue de fato rodar o flow da vítima por esse caminho, porque o Langflow autoriza a execução separadamente. O bug é uma primitiva de ler-e-injetar através da fronteira entre usuários, não um sequestro do flow.

Esta descrição é construída a partir do advisory público e do comportamento documentado do endpoint, não de uma captura contra uma instância real. Teste apenas contra uma instalação que você possui ou está explicitamente autorizado a avaliar.

Linha do tempo

  1. 03/09/2026: a IBM publica o advisory da CVE-2026-17627.
  2. 1.10.3: o Langflow OSS sai com a verificação de propriedade no lugar; é a primeira versão corrigida.
  3. 16/10/2026: advisory modificado pela última vez.
  4. 19/10/2026: este writeup.

Referências