CVE-2026-33017
Langflow < 1.9.0: Unauthenticated RCE via malicious public flow data.
- CVSS 9.3
- CWE-94
- Input Validation and Sanitization
- Remote
Langflow is a tool for building and deploying AI-powered agents and workflows. In versions prior to 1.9.0, 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. This issue has been fixed in version 1.9.0.
- CWE
- CWE-94
- CVSS base score
- 9.3
- Published
- 2026-03-20
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Command Injection
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Langflow
- Fixed by upgrading
- Yes
Solution
Upgrade Langflow to version 1.9.0 or later.
Vulnerable code sample
from flask import Flask, request, jsonify
app = Flask(__name__)
# A mock database to represent stored, legitimate flows
MOCK_DATABASE = {
"legit_flow_id": {
"name": "Legitimate Public Flow",
"data": {
"nodes": [
{
"id": "node1",
"data": {
"code": "print('This is the intended execution of a stored public flow.')"
}
}
]
}
}
}
@app.route("/api/v1/build_public_tmp/<flow_id>/flow", methods=["POST"])
def build_public_tmp_flow(flow_id):
"""
This function simulates the vulnerable endpoint before the fix in version 1.9.0.
It is unauthenticated by design to allow building public flows. The vulnerability
lies in incorrectly accepting and processing an optional 'data' parameter from the
request body, which can contain arbitrary code.
"""
request_data = request.get_json(silent=True) or {}
flow_data_to_process = None
# VULNERABILITY: If 'data' is present in the request body, the server uses
# this attacker-controlled data instead of the legitimate, stored flow data
# associated with the 'flow_id'.
if 'data' in request_data:
# The attacker-controlled data is selected for execution.
flow_data_to_process = request_data.get('data')
else:
# This is the intended behavior for the unauthenticated endpoint:
# fetch the pre-existing public flow from the database.
stored_flow = MOCK_DATABASE.get(flow_id)
if stored_flow:
flow_data_to_process = stored_flow.get('data')
if not flow_data_to_process:
return jsonify({"detail": "Flow not found"}), 404
# The code from the selected flow data is processed. If the attacker
# provided the 'data' parameter, their malicious code is executed here.
if 'nodes' in flow_data_to_process:
for node in flow_data_to_process.get('nodes', []):
code_to_run = node.get('data', {}).get('code')
if code_to_run:
try:
# UNSAFE EXECUTION: Arbitrary code from the flow definition
# is passed directly to exec() without any sandboxing,
# resulting in Remote Code Execution.
exec(code_to_run, {})
except Exception as e:
return jsonify({"error": f"Execution failed: {str(e)}"}), 500
return jsonify({"status": "build initiated"}), 200Patched code sample
import uuid
from typing import Dict, Optional
from fastapi import APIRouter, Body, HTTPException, Path
# This is a mock database to simulate stored, trusted flow data.
# In a real application, this would be a persistent database (e.g., PostgreSQL, MySQL).
MOCK_DATABASE = {
"public_flow_abc123": {
"name": "Official Public Flow",
"data": {
"nodes": [
{
"id": "node-1",
# This is the trusted code stored by the application owner.
"code": "print('Executing the safe, database-stored flow.')"
}
]
}
}
}
router = APIRouter(prefix="/api/v1", tags=["Build"])
def get_flow_from_db(flow_id: str) -> Dict:
"""Simulates fetching a flow securely from the database."""
flow = MOCK_DATABASE.get(flow_id)
if not flow:
raise HTTPException(status_code=404, detail="Flow not found in database.")
return flow
def execute_flow_code(flow_data: Dict):
"""
Simulates the part of the build process that executes code from a flow node.
The `exec` call is the security-sensitive operation (sink).
"""
try:
code_to_run = flow_data["data"]["nodes"][0]["code"]
print(f"Found trusted code to execute: '{code_to_run}'")
# In a real fix, this 'exec' might be further sandboxed or replaced,
# but the primary fix is ensuring the code source is trusted.
exec(code_to_run, {}, {})
print("Execution complete.")
except (KeyError, IndexError) as e:
print(f"Could not find code in the flow data structure: {e}")
except Exception as e:
print(f"An error occurred during code execution: {e}")
@router.post("/build_public_tmp/{flow_id}/flow", status_code=201)
def build_public_tmp_flow_fixed(
flow_id: str = Path(..., description="The unique identifier for the public flow."),
# The `data` parameter from the request body is still defined to handle incoming
# requests gracefully, but its value is intentionally ignored.
data: Optional[Dict] = Body(None, description="[DEPRECATED AND IGNORED] Flow data from the request body."),
):
"""
Builds a public flow, demonstrating the fix for CVE-2026-33017.
The fix ensures that any 'data' payload in the request body is
**completely ignored**. The function exclusively uses the `flow_id` from the URL
to fetch the flow configuration from the trusted database. This prevents an
attacker from supplying arbitrary code in the request body for execution.
"""
if data:
# Log that extraneous data was received but is being ignored. This is a
# critical part of the fix's logic.
print(
"INFO: Request body contained 'data', which is being ignored for security reasons "
"on this public endpoint."
)
# --- CORE OF THE FIX ---
# 1. The flow data is ALWAYS fetched from the database using the flow_id.
# The 'data' variable from the request body is never used.
print(f"Fetching flow configuration for flow_id: {flow_id} from the database.")
trusted_flow_data = get_flow_from_db(flow_id)
# 2. The build and execution process now operates ONLY on the trusted data.
print("Proceeding to build flow using only trusted data from the database.")
execute_flow_code(trusted_flow_data)
return {
"message": "Flow build process initiated using trusted, stored data.",
"flow_id": flow_id,
}
# To run this example:
# 1. Install FastAPI and Uvicorn:
# pip install "fastapi[all]"
# 2. Save the code as a Python file (e.g., `fixed_app.py`).
# 3. Run the server:
# uvicorn fixed_app:app --reload
#
# To test the fix, send a POST request with malicious data:
#
# curl -X POST http://127.0.0.1:8000/api/v1/build_public_tmp/public_flow_abc123/flow \
# -H "Content-Type: application/json" \
# -d '{
# "data": {
# "nodes": [
# {
# "id": "malicious-node",
# "code": "import os; os.system(\"echo VULNERABLE\")"
# }
# ]
# }
# }'
#
# The server output will show that the malicious data was ignored and only the
# safe, database-stored code ("print(...)") was executed.
if __name__ == "__main__":
import uvicorn
from fastapi import FastAPI
app = FastAPI()
app.include_router(router)
print("=" * 80)
print("FastAPI server running. This demonstrates the fix for CVE-2026-33017.")
print("Test by sending a POST request to /api/v1/build_public_tmp/{flow_id}/flow")
print("Example using curl:")
print(
'curl -X POST "http://127.0.0.1:8000/api/v1/build_public_tmp/public_flow_abc123/flow" '
'-H "Content-Type: application/json" '
'-d \'{"data": {"nodes": [{"code": "import os; print(\'PWNED by attacker\')"}]}}\''
)
print("=" * 80)
uvicorn.run(app, host="0.0.0.0", port=8000)Payload
{
"data": {
"nodes": [
{
"id": "rce_node_1",
"data": {
"node": {
"template": {
"_type": "CustomComponent",
"code": {
"value": "import os; os.system('touch /tmp/pwned')"
}
}
}
}
}
],
"edges": []
}
}
Cite this entry
@misc{vaitp:cve202633017,
title = {{Langflow < 1.9.0: Unauthenticated RCE via malicious public flow data.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-33017},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-33017/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
