# REPRO-2026-00268: Langflow’s /api/v1/responses endpoint contains an IDOR that lets any authenticated user execute another user’s flow by supplying the victim’s flow UUID. ## Summary Status: published Severity: critical Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00268 CVE: CVE-2026-55255 ## Package Name: langflow (pip) Ecosystem: pip Affected: < 1.9.1 Fixed: >=1.9.1 ## Root Cause # RCA Report — CVE-2026-55255 ## Summary Langflow's OpenAI-compatible `POST /api/v1/responses` endpoint contains an Insecure Direct Object Reference (IDOR). The helper `get_flow_by_id_or_endpoint_name` resolves a flow by UUID using `session.get(Flow, flow_id)` **without comparing `flow.user_id` to the caller's identity**. Because `/api/v1/responses` passes the authenticated user's id as the second argument but the UUID branch ignores it, any authenticated user can execute any other user's flow by supplying the victim's flow UUID as the request `model` value. The fix (commit `b0afe3d2d6`, PR #12832) adds an ownership check on both the UUID and endpoint-name branches and returns the flow as not-found when the caller is not the owner. ## Impact - **Package/component affected:** `langflow-base` 0.9.0 (`langflow` 1.9.0), helper `langflow.helpers.flow.get_flow_by_id_or_endpoint_name`, reached via `langflow.api.v1.openai_responses.create_response` (`POST /api/v1/responses`). - **Affected versions:** Langflow `< 1.9.1` (advisory) / `< 1.9.2` (NVD). The vulnerable source analyzed here is commit `f52d0f0072` (langflow 1.9.0 / langflow-base 0.9.0), the parent of the fix commit `b0afe3d2d6`. - **Risk level and consequences:** High. An authenticated (non-superuser) attacker can execute arbitrary flows owned by other tenants by UUID, exposing those flows' outputs (cross-tenant data disclosure), consuming the victims' resources, and affecting integrity. This is a cross-tenant authorization bypass. ## Impact Parity - **Disclosed/claimed maximum impact:** Authenticated cross-tenant flow execution / IDOR (authorization bypass) on `/api/v1/responses`. - **Reproduced impact from this run:** An authenticated low-privileged attacker executed a victim-owned flow through the real `POST /api/v1/responses` endpoint by supplying the victim's flow UUID as `model`, receiving a completed response whose `output` contained the flow's executed result. The same request against the fixed build (commit `b0afe3d2d6`) was rejected with `error.code = "flow_not_found"`. - **Parity:** `full` — the claimed IDOR (cross-tenant flow execution) was reproduced end-to-end through the real HTTP API on a running Langflow 1.9.0 server, with a fixed-commit negative control. - **Not demonstrated:** No privilege escalation to superuser, no code execution beyond running an existing flow, and no RCE. The proof demonstrates the authorization bypass (flow execution + output disclosure), which is the claimed impact. ## Root Cause `get_flow_by_id_or_endpoint_name(flow_id_or_name, user_id)` has two branches: ```python # vulnerable (commit f52d0f0072, langflow-base 0.9.0) try: flow_id = UUID(flow_id_or_name) flow = await session.get(Flow, flow_id) # <-- no user_id check except ValueError: endpoint_name = flow_id_or_name stmt = select(Flow).where(Flow.endpoint_name == endpoint_name) if user_id: # <-- only scoped when truthy uuid_user_id = UUID(user_id) if isinstance(user_id, str) else user_id stmt = stmt.where(Flow.user_id == uuid_user_id) flow = (await session.exec(stmt)).first() ``` The UUID branch calls `session.get(Flow, flow_id)` and returns the flow unconditionally — the `user_id` argument is never consulted. The endpoint-name branch only filters by `user_id` when a truthy value is passed. `POST /api/v1/responses` (`openai_responses.create_response`) authenticates the caller via `api_key_security` (the `x-api-key` header) and then resolves the flow with the authenticated user's id: ```python flow = await get_flow_by_id_or_endpoint_name(request.model, str(api_key_user.id)) ``` `request.model` is attacker-controlled. When it parses as a UUID, the vulnerable UUID branch ignores `str(api_key_user.id)` and returns any flow with that UUID, regardless of ownership. The flow is then executed by `run_flow_for_openai_responses`, so the attacker runs the victim's flow and reads its output. The fix (commit `b0afe3d2d6`, PR #12832, "fix(security): close IDOR in get_flow_by_id_or_endpoint_name (LE-639)") normalizes `user_id` once and enforces it on both branches: ```python flow = await session.get(Flow, flow_id) if flow is not None and uuid_user_id is not None and flow.user_id != uuid_user_id: flow = None # cross-user lookup -> treated as not found (404) ``` so the shared `if flow is None: raise HTTPException(404)` fires for cross-tenant lookups and `/api/v1/responses` returns `error.code = "flow_not_found"`. **Fix commit:** `b0afe3d2d6` (PR #12832), file `src/backend/base/langflow/helpers/flow.py`. ## Reproduction Steps 1. **Script:** `bundle/repro/reproduction_steps.sh` (self-contained, idempotent). 2. **What it does:** - Builds the **vulnerable** Langflow 1.9.0 stack (langflow-base 0.9.0) from the repo source at commit `f52d0f0072` (`b0afe3d2d6^`). PyPI never published langflow 1.9.0/1.9.1/1.9.2 (the 1.x series starts at 1.9.3 which already contains the fix), so the vulnerable stack must be built from source. The sandbox ships only Python 3.14 but langflow 1.9.0 requires `<3.14`, so a standalone CPython 3.12 is provisioned via `uv`. - Starts a real Langflow server (multi-user mode, SQLite) on `127.0.0.1:7860`. - Registers a **victim** and an **attacker** user via the public `POST /api/v1/users/` signup endpoint (both `is_superuser: false`). - Logs both in, mints API keys via `POST /api/v1/api_key/`. - The victim creates a flow (ChatInput→ChatOutput echo, no LLM) and captures the victim flow UUID (`VICTIM_FLOW_ID`). - **Ownership proof:** `GET /api/v1/flows/{id}` returns `200` for the victim and `404` for the attacker, confirming the flow is victim-owned and the attacker has no legitimate access. - **IDOR (vulnerable):** the attacker calls `POST /api/v1/responses` with `x-api-key: ` and `{"model": "", "input": "", "stream": false}`. - Swaps in the **fixed** `helpers/flow.py` from commit `b0afe3d2d6`, restarts the server on the **same** SQLite DB (same flow/users), and repeats the attacker call. 3. **Expected evidence of reproduction:** - Vulnerable build: HTTP 200, `object: "response"`, `status: "completed"`, `model` equals the victim flow UUID, and `output[].content[].text` contains the attacker's probe string — the victim's flow was executed by the attacker. - Fixed build: `{"error": {"code": "flow_not_found", "message": "Flow with id '' not found"}}` — the same request is blocked. - The only difference between the two phases is `helpers/flow.py`, proving the divergence is the missing ownership check. ## Evidence - `bundle/logs/reproduction_steps.log` — full run log with version, marker checks, and both IDOR responses. - `bundle/logs/langflow_server_vuln.log` / `bundle/logs/langflow_server_fixed.log` — server startup/health logs for each phase. - `bundle/repro/artifacts/victim_register.json` / `attacker_register.json` — signup responses (`is_superuser: false`). - `bundle/repro/artifacts/victim_flow_create.json` — flow creation response containing `VICTIM_FLOW_ID` and `user_id` (victim). - `bundle/repro/artifacts/ownership_victim_get.txt` / `ownership_attacker_get.txt` — `GET /api/v1/flows/{id}` status codes (200 vs 404). - `bundle/repro/artifacts/idor_response_vuln.json` — vulnerable IDOR response (`status: completed`, probe in output). - `bundle/repro/artifacts/idor_response_fixed.json` — fixed IDOR response (`error.code: flow_not_found`). - `bundle/repro/artifacts/summary.json` — structured classification (`idor_confirmed: true`). - `bundle/repro/runtime_manifest.json` — runtime evidence manifest (`service_started/healthcheck_passed/target_path_reached = true`). - `bundle/repro/validation_verdict.json` — structured verdict. Environment: Langflow 1.9.0 (langflow-base 0.9.0) built from source commit `f52d0f0072`; fixed control commit `b0afe3d2d6`; Python 3.12 (uv standalone); SQLite; uvicorn backend-only; multi-user mode (`LANGFLOW_AUTO_LOGIN=false`, signup enabled). Key excerpt (vulnerable IDOR response): ```json { "object": "response", "status": "completed", "error": null, "model": "", "output": [{ "type": "message", "status": "completed", "content": [{ "type": "output_text", "text": "IDOR_PROBE_VULN_..." }] }] } ``` Key excerpt (fixed IDOR response): ```json { "error": { "message": "Flow with id '' not found", "type": "invalid_request_error", "code": "flow_not_found" } } ``` ## Recommendations / Next Steps - **Upgrade** to Langflow >= 1.9.1 (contains the fix from PR #12832 / commit `b0afe3d2d6`). - **Fix approach (already applied upstream):** enforce `user_id` on **both** branches of `get_flow_by_id_or_endpoint_name` and return the flow as not-found on cross-user lookups, so the shared 404 path fires and flow existence is not disclosed to unauthorized callers. - **Defense in depth:** audit other callers of `get_flow_by_id_or_endpoint_name` (e.g. webhook/run endpoints) for the same ownership gap; add an integration test that asserts a cross-tenant UUID lookup returns 404 (the upstream regression tests in `tests/unit/helpers/test_flow_helpers.py` cover this). - **Testing recommendations:** add a multi-tenant API test that creates a flow as user A and asserts user B cannot execute it via `/api/v1/responses` by UUID. ## Additional Notes - **Idempotency:** The script extracts the vulnerable source fresh each run (so the editable `langflow-base` install always starts from the vulnerable `flow.py`), reuses a cached venv at the durable project cache path when present, and uses a fresh SQLite DB each run. Consecutive runs produce the same vulnerable/completed vs fixed/flow_not_found divergence. - **Note on request field:** the advisory's example curl uses `"input_value"`, but the actual `OpenAIResponsesRequest` schema field is `"input"`. The reproduction uses the correct `input` field; the IDOR is independent of this field (it is triggered by `model` = victim flow UUID). - **Note on HTTP status:** both the vulnerable (flow executed) and the fixed (flow not found) responses are returned with HTTP 200 by the OpenAI-compatible endpoint; the discriminator is the response **body** (`status: completed` + `output` vs `error.code: flow_not_found`), which is a stronger signal than the status code. - **Scope:** The vulnerable `wt-vuln`/`wt-fixed` worktrees provided in the project cache already contained the fix commit, so the reproduction anchors the vulnerable checkout to `b0afe3d2d6^` (`f52d0f0072`) per the fixed-commit checkout rule, and the fixed checkout to `b0afe3d2d6`. ## Reproduction Details Reproduced: 2026-07-08T04:48:34.268Z Duration: 1709 seconds Tool calls: 241 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00268 pruva-verify CVE-2026-55255 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00268&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00268/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-55255 - Source: https://nvd.nist.gov/vuln/detail/CVE-2026-55255 ## Artifacts - bundle/repro/reproduction_steps.sh (reproduction_script, 24153 bytes) - bundle/repro/rca_report.md (analysis, 10863 bytes) - bundle/vuln_variant/reproduction_steps.sh (reproduction_script, 24588 bytes) - bundle/vuln_variant/rca_report.md (analysis, 13416 bytes) - bundle/artifact_promotion_manifest.json (other, 15834 bytes) - bundle/artifact_promotion_report.json (other, 22020 bytes) - bundle/vuln_variant/source_identity.json (other, 2473 bytes) - bundle/vuln_variant/root_cause_equivalence.json (other, 2551 bytes) - bundle/repro/runtime_manifest.json (other, 1081 bytes) - bundle/repro/validation_verdict.json (other, 1012 bytes) - bundle/repro/artifacts/idor_response_vuln.json (other, 718 bytes) - bundle/repro/artifacts/idor_response_fixed.json (other, 141 bytes) - bundle/repro/artifacts/summary.json (other, 386 bytes) - bundle/logs/reproduction_steps.log (log, 3365 bytes) - bundle/logs/langflow_server_vuln.log (log, 3302 bytes) - bundle/logs/langflow_server_fixed.log (log, 2523 bytes) - bundle/repro/artifacts/victim_register.json (other, 332 bytes) - bundle/repro/artifacts/attacker_register.json (other, 334 bytes) - bundle/repro/artifacts/victim_flow_create.json (other, 21950 bytes) - bundle/repro/artifacts/ownership_victim_get.txt (other, 62 bytes) - bundle/repro/artifacts/ownership_attacker_get.txt (other, 64 bytes) - bundle/vuln_variant/artifacts/variant_response_vuln.json (other, 446 bytes) - bundle/vuln_variant/artifacts/variant_response_fixed.json (other, 232 bytes) - bundle/vuln_variant/artifacts/summary.json (other, 763 bytes) - bundle/logs/vuln_variant/reproduction_steps.log (log, 14182 bytes) - bundle/vuln_variant/validation_verdict.json (other, 1458 bytes) - bundle/vuln_variant/variant_manifest.json (other, 5158 bytes) - bundle/vuln_variant/patch_analysis.md (documentation, 10357 bytes) - bundle/vuln_variant/runtime_manifest.json (other, 1297 bytes) - bundle/vuln_variant/artifacts/ownership_victim_get.txt (other, 62 bytes) - bundle/vuln_variant/artifacts/victim_flow_create.json (other, 21956 bytes) - bundle/logs/vuln_variant/langflow_server_vuln.log (log, 3712 bytes) - bundle/logs/vuln_variant/langflow_server_fixed.log (log, 2523 bytes) - bundle/vuln_variant/artifacts/victim_register.json (other, 334 bytes) - bundle/vuln_variant/artifacts/attacker_register.json (other, 336 bytes) - bundle/vuln_variant/artifacts/ownership_attacker_get.txt (other, 64 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00268 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00268/artifacts/bundle/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00268 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev