Two 9.9s, One Sink, Three Different “Fixed In” Versions — Langflow’s MCP Stdio RCE

On 5 October 2026, two CVE records for Langflow were published 30 minutes apart. CVE-2026-105697 went out at 20:16 UTC and CVE-2026-105740 at 20:46 UTC. Both carry CVSS 9.9 on the same vector string, both are CWE-78, and both describe the identical thing: the Model Context Protocol stdio transport launching a user-supplied command as an operating-system process. They are the same bug at the same sink.

They do not agree on when it was fixed. CVE-2026-105740 says the flaw affects versions “prior to 1.9.0” and “is fixed in 1.9.0.” CVE-2026-105697 says the affected range is >= 1.1.2, < 1.10.3 and that the fix is 1.10.3. An operator who patched to 1.9.0 on the strength of the first record spent four more releases exposed. We read the source at four release tags to work out which record is right, and the answer is the more alarming one — with a wrinkle that neither record states cleanly.

What the two advisories actually claim

Both CVEs were assigned by GitHub from repository advisories whose GitHub publication dates predate the CVE records: GHSA-7w94-79vh-5mr2 (→ CVE-2026-105740) went up on 10 September 2026 and lists exactly one affected version, 1.8.3; GHSA-w794-rj3p-xv45 (→ CVE-2026-105697) went up on 28 September 2026 with the full 1.1.2 – 1.10.2 range across three PyPI packages (langflow, langflow-base < 0.10.3, and lfx < 1.10.3). Both were edited on 5 October, minutes before their CVE records appeared.

The second advisory is unusually candid about why there are two numbers. It states the issue “was fixed in two steps”: pull request #12290 in 1.9.0 added a command allowlist to the REST model, and pull request #14036 in 1.10.3 applied the same policy at the execution sink. In other words, CVE-2026-105740 is not wrong about its reported vector — the “Add MCP Server” settings page — and is wrong about the vulnerability class. That distinction is invisible to anyone reading a severity feed.

Reading the code at four tags

We fetched src/lfx/src/lfx/base/mcp/util.py and src/backend/base/langflow/api/v2/schemas.py at v1.9.0, v1.10.0, v1.10.2 and v1.10.3 from the project repository. The sequence is cleaner than either advisory describes:

  • v1.9.0 — allowlist at the door, nothing at the sink. schemas.py defines ALLOWED_MCP_COMMANDS and a validate_command field validator. util.py contains no reference to any validation helper, and _connect_to_server still builds StdioServerParameters(command="bash", args=["-c", f"exec {command_str} || echo ..."]) on POSIX and a cmd /c equivalent on Windows. Anything that reaches the launcher without passing through the Pydantic model — a config embedded in a flow, a tweak, the MCP Tools component — executes unchecked.
  • v1.10.0 — unchanged. Same bash -c wrapper, same absent sink validation.
  • v1.10.2 — the shell goes away, the gap stays. The launcher now execs the binary directly with structured arguments and an explicit shell=False comment noting that “no shell interpreter is ever interposed.” That removes metacharacter injection. It does not add validation: validate_mcp_stdio_config still appears nowhere in the file, so an arbitrary executable is still spawnable, just without a shell to chain through.
  • v1.10.3 — the sink is finally guarded. util.py imports validate_mcp_stdio_config from lfx.base.mcp.security and calls it at three points, including immediately before spawn. ALLOWED_MCP_COMMANDS has moved out of the API schema into the shared security module so one policy serves both paths.

This matters for a claim in CVE-2026-105697 itself. The record says 1.10.3 “removed the bash -c wrapper.” The code says the wrapper was already gone in 1.10.2, published to PyPI on 7 July 2026, sixteen days before 1.10.3. What 1.10.3 added was the missing authorization check at the sink. The distinction is not pedantry: a defender who reads “removed the shell wrapper” as the fix, and who confirms the wrapper is absent in their deployed 1.10.2, will conclude they are patched. They are not.

The default that makes this unauthenticated

Both records describe the bug as requiring privileges — CVSS PR:L, any authenticated non-admin user. The advisory then notes that with the default LANGFLOW_AUTO_LOGIN=true, GET /api/v1/auto_login issues a token with no credentials at all. On an exposed instance running stock configuration, the privilege requirement evaporates. Langflow documents AUTO_LOGIN as a development-only setting; the exploitation record of this project suggests that documentation is not doing the work. CISA added a Langflow origin-validation flaw to the Known Exploited Vulnerabilities catalog in May, and the platform has been a recurring target since.

An allowlist that allows bash

Worth reading the policy before trusting it. ALLOWED_MCP_COMMANDS at 1.10.3 is {node, python, python3, npx, uvx, docker, cmd, sh, bash}. Three of those nine entries are shells. The maintainers handle this by treating cmd/sh/bash as wrappers and recursively validating the wrapped payload with an explicit depth bound — a defensible design given that Langflow’s own starter projects ship sh -c uvx ... invocations, but one whose safety rests entirely on the recursive parser being complete.

Two follow-up fixes in the same release window show the parser was not complete on the first attempt. PR #14122 (merged 16 July) found that the Docker policy inspected arguments for host-access flags without first requiring the run subcommand, so docker cp reached the daemon unchecked; its own description notes that deployments leaving LANGFLOW_MCP_SERVER_DOCKER_HARDENING=false with daemon access were affected, while published Langflow images enable hardened mode. PR #14149 (17 July) then denied non-superusers local stdio configuration entirely when custom code execution is locked down. An allowlist containing a container runtime and three shells is a parser, and parsers accumulate CVEs — the same structural problem as the shell-operator bypass in Ollama’s agent approval gate.

Two more MCP records in the same batch

  • CVE-2026-105741 (CVSS 7.1, CWE-290) — get_client_ip trusted the leftmost, fully client-controlled entry of X-Forwarded-For with no trusted-proxy check, letting a remote attacker spoof 127.0.0.1 and pass the “local-only” gate on POST /api/v1/mcp/project/{project_id}/install to write an MCP client config on the server filesystem. Affects 1.5.0 to 1.10.2. The advisory includes an explicit correction to the original report, noting the destination path is not attacker-supplied but resolved from a fixed set of per-OS developer-tool paths. Credit to the maintainers for publishing the correction rather than the louder version.
  • CVE-2026-105699 (CVSS 7.1, CWE-639) — project-scoped MCP connections authenticated the project_id in the URL but never authorized the URI passed to resources/read, so handle_read_resource parsed an attacker-supplied flow_id and called storage_service.get_file across tenants. Affects 1.6.8 through 1.9.0, fixed in 1.9.1. Authorization at connection time, not at resource time — precisely the missing field-level control that MLCommons named in its privacy taxonomy last week.

The pattern worth naming

Langflow’s 1.9.0 fix was a real fix of a real vector, shipped quickly after a good report. It was also validation attached to one data path into a sink that had several. That shape — guard the API model, leave the executor open — is the most common way an AI-platform patch under-delivers, because the REST schema is where reviewers look and the component loader is not. The same gap produced the MySQL MCP server’s unauthenticated SSE path, and it is why “patched” needs to mean “patched at the sink.”

The second-order problem is the metadata. Two 9.9 records, published half an hour apart, describing one bug, pointing at different fixed versions, is a scanner-accuracy problem before it is a disclosure problem. We have now seen three variants of this failure in six weeks — an off-by-one patched version in a Grafana MCP PoC, a malware advisory naming the wrong dependency, and now a duplicate pair that disagrees with itself. Vulnerability data is becoming the weak link in the AI supply chain.

What to do

  • Treat 1.10.3 as the floor, not 1.9.0. If your inventory says “Langflow ≥ 1.9.0, not vulnerable” because it matched CVE-2026-105740, re-evaluate against CVE-2026-105697’s range. Current PyPI release is 1.12.4; langflow-base needs ≥ 0.10.3 and lfx ≥ 1.10.3 as separate pins.
  • Check 1.10.2 specifically. It is the trap version: the shell wrapper is visibly gone, so a code-level spot check looks clean, but the sink has no validation.
  • Disable LANGFLOW_AUTO_LOGIN on anything reachable. It is the difference between an authenticated-user bug and an internet-facing unauthenticated RCE, and it is on by default.
  • Audit stdio MCP configs embedded in flows, not just in settings. The vector that survived 1.9.0 was the one that never touched the REST model. Enumerate MCP Tools components and tweak payloads too.
  • Set LANGFLOW_MCP_SERVER_DOCKER_HARDENING and the code-execution lockdown. Published Docker images enable hardened mode; source and custom deployments may not. If the Langflow process can reach a Docker socket, the allowlist’s docker entry is a host-compromise primitive.
  • Do not grant stdio MCP configuration to ordinary users. The 1.10.3-era fix restricts it to superusers under lockdown. Make that your policy regardless of version.

Verification note: we read the CVE records for CVE-2026-105697, CVE-2026-105740, CVE-2026-105741 and CVE-2026-105699 from the MITRE CVE Services API (publication timestamps 20:16:58Z, 20:46:18Z, 20:56:00Z and 20:37:59Z on 5 October 2026), and the corresponding GitHub repository advisories for their original publication dates, version ranges and full descriptions. The version analysis is ours: we fetched src/lfx/src/lfx/base/mcp/util.py and src/backend/base/langflow/api/v2/schemas.py at tags v1.9.0, v1.10.0, v1.10.2 and v1.10.3 and confirmed the presence of the bash -c "exec ..." wrapper through v1.10.0, its absence in v1.10.2, and the first appearance of validate_mcp_stdio_config calls in v1.10.3. Release and merge dates come from the GitHub releases and pull-request APIs; PyPI upload timestamps (1.10.2 on 7 July 2026, 1.10.3 on 23 July 2026) and the current 1.12.4 release come from the PyPI JSON API. We did not test any exploit and did not contact the maintainers before publication.

Sources: