Skip to content

CVE-2026-33017: Verified Repro With Script Download

CVE-2026-33017: Unauthenticated RCE in Langflow via public flow build endpoint

CVE-2026-33017 is verified against langflow · pip. Affected versions: <= 1.8.2 (all versions < 1.9.0). Fixed in 1.9.0. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00201.

REPRO-2026-00201 langflow · pip RCE Variant found Jul 2, 2026 CVE entry ↗ .txt
Severity
CRITICAL
CVSS
9.8
Confidence
HIGH
Reproduced in
25m 8s
Tool calls
259
Spend
$5.45
01 · Overview

What Is CVE-2026-33017?

CVE-2026-33017 is a critical unauthenticated remote code execution vulnerability in Langflow, reachable via the public flow-build endpoint with a single HTTP request. It was added to the CISA Known Exploited Vulnerabilities catalog on 2026-03-25. Pruva reproduced it (reproduction REPRO-2026-00201).

02 · Severity & CVSS

CVE-2026-33017 Severity & CVSS Score

CVE-2026-33017 is rated critical severity, with a CVSS base score of 9.8 out of 10.

CRITICAL threat level
9.8 / 10 CVSS base
Weakness CWE-94, CWE-95, CWE-306 — Improper Control of Generation of Code ('Code Injection')

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected langflow Versions

langflow · pip versions <= 1.8.2 (all versions < 1.9.0) are affected.

How to Reproduce CVE-2026-33017

$ pruva-verify REPRO-2026-00201
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00201/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-33017

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

JSON body with malicious custom component code (top-level os.system) in POST /api/v1/build_public_tmp/{flow_id}/flow

Attack chain
  1. /api/v1/build_public_tmp/{flow_id}/flow
  2. start_flow_build
  3. build_graph_from_data
  4. create_class
  5. prepare_global_scope
  6. exec
Runnable proof: reproduction_steps.sh
Captured evidence: container fixed 1result fixed 1 stderrcontainer fixed 2result fixed 2 stderrcontainer claimed fixed 1result claimed fixed 1 stderrcontainer claimed fixed 2result claimed fixed 2 stderr
Variants tested

Bypass of the CVE-2026-33017 v1.9.0 fix: the unauthenticated public flow build path (POST /api/v1/build_public_tmp/{flow_id}/flow) no longer accepts a client-supplied `data` parameter, but it loads the PUBLIC flow definition from the database and exec()'s each node's stored custom-component `code` at graph-build time…

How the agent worked 641 events · 259 tool calls · 25 min
25 minDuration
259Tool calls
170Reasoning steps
641Events
1Dead-ends
Agent activity over 25 min
Support
14
Hypothesis
2
Repro
450
Judge
23
Variant
148
0:0025:08

Root Cause and Exploit Chain for CVE-2026-33017

Versions: langflow < 1.9.0 (reproduction uses 1.8.1 as the

CVE-2026-33017 is an unauthenticated remote code execution (RCE) vulnerability in Langflow prior to version 1.9.0. The public flow-build endpoint POST /api/v1/build_public_tmp/{flow_id}/flow accepts an attacker-controlled data parameter (FlowDataRequest) containing arbitrary Python code inside a custom component node. Because the endpoint is intentionally unauthenticated for public flows, any remote attacker can reach it. The supplied flow definition is passed through start_flow_build()build_graph_from_data()Graph.from_payload() and ultimately to the custom-component loader, which extracts the code field and executes it with exec() inside prepare_global_scope() (in lfx/custom/validate.py) without any sandboxing. A module-level assignment such as _rce = os.system(...) is an ast.Assign node that prepare_global_scope() collects and exec()s at graph-build time, yielding arbitrary command execution with the privileges of the Langflow server process. A single HTTP request is sufficient.

  • Product: Langflow (PyPI package langflow; Docker image langflowai/langflow)
  • Affected versions: langflow < 1.9.0 (reproduction uses 1.8.1 as the vulnerable image).
  • Patched versions: >= 1.9.0 (the public build endpoint hardcodes data=None and loads the stored flow from the database only).
  • Risk level: Critical (CISA KEV added 2026-03-25).
  • Consequences: An unauthenticated, remote attacker can run arbitrary system commands, read environment variables (including LLM API keys / cloud credentials), access/modify the database and flow data, and establish persistence. The server process ran as uid=1000(user) gid=0(root) in the container image.

Impact Parity

  • Disclosed/claimed maximum impact: Unauthenticated remote code execution (code execution) via a single HTTP request to the public build endpoint.
  • Reproduced impact from this run: Confirmed code execution. The vulnerable langflowai/langflow:1.8.1 container wrote /tmp/rce-proof containing the output of the id command (uid=1000(user) gid=0(root) groups=0(root)) plus a unique per-attempt token, after receiving an unauthenticated HTTP POST to /api/v1/build_public_tmp/{flow_id}/flow. The fixed langflowai/langflow:1.9.0 container did not write the proof file under the identical request (negative control).
  • Parity: full.
  • Not demonstrated: None relevant; the claimed unauthenticated-RCE impact was directly demonstrated end-to-end against the real product.

Root Cause

The vulnerable endpoint build_public_tmp in src/backend/base/langflow/api/v1/chat.py (v1.8.1) declares an inbound data: FlowDataRequest parameter and forwards it directly to start_flow_build():

@router.post("/build_public_tmp/{flow_id}/flow")
async def build_public_tmp(..., data: Annotated[FlowDataRequest | None, Body(embed=True)] = None, ...):
    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, ...)

start_flow_build() (src/backend/base/langflow/api/build.py) builds the graph from the attacker-supplied data when it is present:

async def create_graph(...):
    if not data:
        return await build_graph_from_db(...)
    return await build_graph_from_data(flow_id=..., payload=data.model_dump(), ...)

build_graph_from_data()Graph.from_payload() constructs vertices from the attacker nodes. For a custom component (template._type == "Component"), the loader calls create_class(code, class_name) in src/lfx/src/lfx/custom/validate.py, which calls prepare_global_scope(module). That function iterates the module body, collects top-level ast.Assign / ast.AnnAssign / ast.ClassDef / ast.FunctionDef nodes into definitions, compiles them, and runs:

if definitions:
    combined_module = ast.Module(body=definitions, type_ignores=[])
    compiled_code = compile(combined_module, "<string>", "exec")
    exec(compiled_code, exec_globals)   # <-- attacker module-level code runs here

Therefore a top-level _rce = os.system("id > /tmp/rce-proof ...") executes during graph construction, before any output is produced.

The only access control on the endpoint is verify_public_flow_and_get_user(), which merely checks that the targeted flow_id is marked PUBLIC in the database and that a client_id cookie is present (any value). The attacker creates the public flow themselves (using the AUTO_LOGIN superuser session), so this check is satisfied trivially.

Fix (v1.9.0): the endpoint no longer accepts a data parameter and hardcodes data=None, so the build always loads the stored flow definition from the database. It also validates the stored flow with validate_flow_for_current_settings() and rejects custom components on the public path (CustomComponentValidationError → HTTP 400). The diff is the removal of data: ... = None from the signature and data=datadata=None in the start_flow_build(...) call.

# v1.9.0
job_id = await start_flow_build(flow_id=new_flow_id, source_flow_id=flow_id,
    ..., data=None,  # Always None - public flows load from database only
    ...)

Reproduction Steps

  1. The reproduction is fully automated by bundle/repro/reproduction_steps.sh (with helper bundle/repro/repro_attempt.py).
  2. The script pulls langflowai/langflow:1.8.1 (vulnerable) and langflowai/langflow:1.9.0 (fixed), then runs 2 vulnerable and 2 fixed isolated attempts. Each attempt:
    • starts a fresh Langflow container with LANGFLOW_AUTO_LOGIN=true and --backend-only,
    • waits for the /health endpoint,
    • performs GET /api/v1/auto_login to obtain a superuser access token,
    • creates a PUBLIC flow via POST /api/v1/flows/,
    • sends the unauthenticated exploit POST /api/v1/build_public_tmp/{flow_id}/flow with a client_id cookie and a body whose data contains one CustomComponent node whose code holds a top-level _rce = os.system("id > /tmp/rce-proof && echo RCE_CONFIRMED <token> >> /tmp/rce-proof"),
    • polls for /tmp/rce-proof inside the container and copies it out as evidence, then tears the container down.
  3. Expected evidence: on the vulnerable image each attempt produces logs/proof_vuln_N.txt containing the id output and the unique token, with exploit_status: 200 and proof_exists: true in logs/result_vuln_N.json. On the fixed image proof_exists: false for every attempt (logs/result_fixed_N.json).

Evidence

  • bundle/logs/reproduction_steps.log — full orchestrator log.
  • bundle/logs/result_vuln_{1,2}.json — per-attempt JSON results for the vulnerable image (auto_login=200, create_flow=201, exploit=200, proof_exists=true, proof_content with uid=1000(user)... + token).
  • bundle/logs/proof_vuln_{1,2}.txt — the proof file exfiltrated from the vulnerable container (id output + RCE_CONFIRMED <token>).
  • bundle/logs/result_fixed_{1,2}.json — per-attempt JSON results for the fixed image (exploit=200 but proof_exists=false).
  • bundle/logs/container_{vuln,fixed}_{1,2}.log — container startup/runtime logs.
  • bundle/repro/runtime_manifest.json — structured runtime evidence (entrypoint_kind=api_remote, service_started=true, healthcheck_passed=true, target_path_reached=true).
  • bundle/repro/validation_verdict.json — structured verdict.

Key excerpt from a manual run against langflowai/langflow:1.8.1:

{"role":"vuln","token":"manualtest1","auto_login_status":200,"create_flow_status":201,
 "flow_id":"b43e6614-...","exploit_status":200,
 "exploit_body":"{\"job_id\":\"8449e0de-...\"}","proof_exists":true,
 "proof_content":"uid=1000(user) gid=0(root) groups=0(root)\nRCE_CONFIRMED manualtest1",
 "success":true}

Negative control against langflowai/langflow:1.9.0 (identical request):

{"role":"fixed","token":"fixedtest1","auto_login_status":200,"create_flow_status":201,
 "exploit_status":200,"proof_exists":false,"proof_content":null,"success":false}

Environment: Docker 29.6.1; official images langflowai/langflow:1.8.1 and :1.9.0; exploit executed via docker exec inside each container (the sandbox cannot reach published host ports, so all HTTP traffic is generated inside the container against http://127.0.0.1:7860). No sanitizers were used; this is a non-sanitized production-path proof.

Recommendations / Next Steps

  • Upgrade to Langflow >= 1.9.0 immediately. The public build endpoint no longer accepts client-supplied flow definitions and validates stored flows.
  • If AUTO_LOGIN must stay enabled in production, restrict network exposure of the Langflow HTTP port and place it behind an authenticated reverse proxy; AUTO_LOGIN issues a superuser session without credentials.
  • Consider disabling custom components entirely on public flows (allow_custom_components=false) and enforce access_type=PRIVATE by default.
  • Add an integration test that posts a custom-component payload with a module-level side-effect sentinel to build_public_tmp and asserts it never fires, to prevent regressions of this fix.

Additional Notes

  • Idempotency: The script removes any prior /tmp/rce-proof at the start of each attempt and tears down the container afterwards, so consecutive runs are clean and reproducible. Verified by running two vulnerable and two fixed attempts back-to-back.
  • The malicious payload is delivered as a top-level assignment (_rce = os.system(...)) rather than a bare expression, because prepare_global_scope() only exec()s nodes it classifies as ast.Assign / ast.AnnAssign / ast.ClassDef / ast.FunctionDef; an assignment is guaranteed to execute at graph-build time.
  • The flow created for the exploit uses a benign empty data ({"nodes":[],"edges":[]}) simply to satisfy the access_type=PUBLIC requirement; on the vulnerable path the attacker-supplied data overrides the stored definition, so the stored content is irrelevant.
  • The proof file is written inside the container filesystem and copied out via docker cp for durable evidence.

Variant Analysis & Alternative Triggers for CVE-2026-33017

Versions: versions (as tested): langflowai/langflow:1.9.0 (CVE's "fixed"

CVE-2026-33017 was fixed (per the ticket and the repro) in Langflow 1.9.0 by removing the client-supplied data parameter from the unauthenticated public flow-build endpoint POST /api/v1/build_public_tmp/{flow_id}/flow and hardcoding data=None so the build always loads the flow definition from the database. This variant is a true bypass of that fix: the same unauthenticated public build path still exec()'s each node's stored custom-component code at graph-build time via prepare_global_scope()/eval_custom_component_code, and the only validator the fix added (validate_flow_for_current_settings) is a no-op under the default allow_custom_components=true. By first storing a malicious CustomComponent inside a PUBLIC flow (via POST /api/v1/flows/ using the AUTO_LOGIN superuser token — the exact capability the original CVE repro already uses to create its public flow), an attacker triggers the public build with nothing but a client_id cookie and obtains unauthenticated RCE on the CVE's "fixed" langflowai/langflow:1.9.0. The bypass was confirmed empirically (2/2 attempts: exploit_status=200, proof file written with uid=1000(user) gid=0(root)). It is closed only in v1.10.1 by the upstream follow-up commit 626365f088 ("run trusted server code on unauthenticated public flow builds", the H1-3754930 follow-up); versions 1.9.0 through 1.10.0 remain vulnerable.

Fix Coverage / Assumptions

Invariant the original (v1.9.0) fix relies on: "Public flows never accept client-supplied data, and the stored flow definition is benign, and validate_flow_for_current_settings will block any custom component that should not run on the public path."

Code path(s) it explicitly covers:

  • The in-request data body field of POST /api/v1/build_public_tmp/{flow_id}/flow (src/backend/base/langflow/api/v1/chat.py:640): the data parameter was removed and data=None hardcoded (chat.py:720), so attacker flow data can no longer be supplied in the request body. source_flow_id=flow_id (chat.py:717) forces the build to load the flow from the DB via build_graph_from_db (src/backend/base/langflow/api/build.py:305-313).

What the fix does NOT cover (the gap):

  • The stored-data injection vector. POST /api/v1/flows/ (src/backend/base/langflow/api/v1/flows.py:86, _new_flow) performs no custom-component validation when persisting a PUBLIC flow, so an attacker can store a flow whose data contains an arbitrary-code CustomComponent.
  • The validator it relies on is a no-op under the default configuration. validate_flow_for_current_settingscheck_flow_and_raise (src/lfx/src/lfx/utils/flow_validation.py:158-166) returns immediately when allow_custom_components is True, and allow_custom_components defaults to True (src/lfx/src/lfx/services/settings/base.py:386). The stored malicious custom component is therefore not blocked.
  • Consequently the public build still exec()'s the node's stored code at graph-build time (src/lfx/src/lfx/custom/validate.py:218-222).

Variant / Alternate Trigger

Bypass path (same root cause, same sink, different injection point):

  1. GET /api/v1/auto_login → superuser access_token (no credentials; LANGFLOW_AUTO_LOGIN=true, identical to the parent repro's setup).
  2. POST /api/v1/flows/ with {"name": ..., "access_type": "PUBLIC", "data": <malicious_graph>} — store a PUBLIC flow whose data contains one CustomComponent node whose code has a top-level _rce = os.system("id > /tmp/rce-proof ... && echo RCE_CONFIRMED <token> >> ..."). The top-level assignment is an ast.Assign node that prepare_global_scope() collects and exec()'s at graph-build time, before any component method runs.
  3. POST /api/v1/build_public_tmp/{flow_id}/flow with only a client_id cookie and no data field in the body. The server loads the stored (malicious) flow from the DB and exec()'s the node code → RCE.

Entry point: POST /api/v1/build_public_tmp/{flow_id}/flow (unauthenticated; client_id cookie only), with the malicious payload pre-staged in the DB via the authenticated POST /api/v1/flows/ flow-create endpoint (AUTO_LOGIN token).

Specific code path: chat.py:build_public_tmp (640) → validate_flow_for_current_settings(flow.data) (no-op, 711) → start_flow_build(data=None, source_flow_id=flow_id) (717-720) → build.py:generate_flow_events/create_graph (305-313) → build_graph_from_dbGraph.from_payloadcreate_classprepare_global_scope (custom/validate.py:218-222) → exec().

  • Package/component: Langflow (langflow PyPI; langflowai/langflow Docker image). Sensitive component: the unauthenticated public flow-build endpoint and the custom-component loader (lfx/custom/validate.py).
  • Affected versions (as tested): langflowai/langflow:1.9.0 (CVE's "fixed" version) is still vulnerable. Source analysis shows the bypass affects 1.9.0 through 1.10.0 (all versions that have the original fix but lack the H1-3754930 follow-up). The bypass is closed in 1.10.1.
  • Risk level: Critical — unauthenticated remote code execution with a single HTTP request to the public build endpoint (plus one authenticated flow-create request that needs no credentials under AUTO_LOGIN=true). The server process ran as uid=1000(user) gid=0(root) in the container.
  • Consequences: Arbitrary command execution as the Langflow server user, enabling exfiltration of LLM API keys / cloud credentials, database/flow tampering, and persistence — identical to the parent CVE.

Impact Parity

  • Disclosed/claimed maximum impact (parent CVE): Unauthenticated remote code execution via the public build endpoint.
  • Reproduced impact from this variant run: Unauthenticated remote code execution via the public build endpoint on the CVE's "fixed" 1.9.0 (proof file containing id output + per-attempt token, exploit_status=200).
  • Parity: full. The variant achieves the same unauthenticated RCE as the parent CVE, on the version the CVE claims is fixed.
  • Not demonstrated: Beyond os.system command execution, no separate read/write primitive was exercised (same scope as the parent repro).

Root Cause

The underlying bug — exec() of attacker-controlled custom-component code on the unauthenticated public build path with no sandboxing — is unchanged. The v1.9.0 fix only relocated the trust boundary: it stopped trusting the request body data, but continued trusting the database-stored data and continued exec()'ing its node code. Because the only validator guarding the stored data (validate_flow_for_current_settings) short-circuits on the default allow_custom_components=true, and because flow creation (POST /api/v1/flows/) performs no custom-component validation, an attacker can smuggle arbitrary code into a PUBLIC flow and have the unauthenticated public build execute it.

Fix commit that closes this variant (upstream follow-up): 626365f088379236776e0d72f7d18c9094e43ebb"fix(security): run trusted server code on unauthenticated public flow builds (#13540)". It is an ancestor of v1.10.1 (commit a66b75ac26) but not of v1.10.0/v1.9.0 (git merge-base --is-ancestor verified). Its own message confirms the gap: "… a public flow containing a plain CustomComponent — or any node carrying arbitrary code — would execute on that path without authentication (follow-up to H1-3754930)."

Reproduction Steps

  1. The bypass is fully automated by bundle/vuln_variant/reproduction_steps.sh (helper bundle/vuln_variant/variant_attempt.py, run inside each container).
  2. The script pulls langflowai/langflow:1.9.0 (CVE "fixed") and langflowai/langflow:1.10.1 (follow-up fixed), records each image's exact version/identity, then runs 2 attempts against 1.9.0 and 2 attempts against 1.10.1. Each attempt:
    • starts a fresh container with LANGFLOW_AUTO_LOGIN=true --backend-only,
    • waits for /health,
    • GET /api/v1/auto_login → superuser token,
    • POST /api/v1/flows/ → creates a PUBLIC flow whose stored data carries a CustomComponent with a top-level _rce = os.system(...) payload,
    • POST /api/v1/build_public_tmp/{flow_id}/flow with only a client_id cookie and no data body field,
    • polls for /tmp/rce-proof and copies it out as evidence, then tears the container down.
  3. Expected evidence (observed):
    • On 1.9.0: each attempt writes logs/vuln_variant/proof_claimed_fixed_N.txt with uid=1000(user) gid=0(root) groups=0(root) + RCE_CONFIRMED <token>, and logs/vuln_variant/result_claimed_fixed_N.json shows exploit_status: 200, proof_exists: true, success: true.
    • On 1.10.1: logs/vuln_variant/result_followup_fixed_N.json shows exploit_status: 400, exploit_body: {"detail":"This flow cannot be executed."}, proof_exists: false, success: false.
  4. Exit semantics: exit 0 = bypass reproduced on the CVE-claimed-fixed 1.9.0 (true bypass of the claimed fix) and not reproduced on 1.10.1.

Evidence

  • bundle/vuln_variant/reproduction_steps.sh — orchestrator (idempotent; verified by two consecutive runs, both exit 0).
  • bundle/vuln_variant/variant_attempt.py — in-container exploit helper.
  • bundle/logs/vuln_variant/reproduction_steps.log — full orchestrator log.
  • bundle/logs/vuln_variant/result_claimed_fixed_{1,2}.json — 1.9.0 per-attempt results (auto_login=200, create_flow=201, exploit=200, proof_exists=true).
  • bundle/logs/vuln_variant/proof_claimed_fixed_{1,2}.txt — exfiltrated proof files from the 1.9.0 containers.
  • bundle/logs/vuln_variant/result_followup_fixed_{1,2}.json — 1.10.1 per-attempt results (exploit=400 "This flow cannot be executed.", proof_exists=false).
  • bundle/logs/vuln_variant/container_{claimed,followup}_fixed_{1,2}.log — container startup/runtime logs.
  • bundle/logs/vuln_variant/{claimed_fixed,followup_fixed}_image_identity.txt — exact tested image version metadata (pip 1.9.0 / 1.10.1).
  • bundle/vuln_variant/runtime_manifest.json, bundle/vuln_variant/validation_verdict.json, bundle/vuln_variant/source_identity.json, bundle/vuln_variant/root_cause_equivalence.json, bundle/vuln_variant/variant_manifest.json — structured records.

Key excerpts (1.9.0, bypass succeeds):

{"role":"claimed_fixed","mode":"bypass","token":"dde4c416676ff815","endpoint":"public_stored",
 "auto_login_status":200,"create_flow_status":201,"flow_id":"c9522932-...","exploit_status":200,
 "exploit_body":"{\"job_id\":\"eadbad1c-...\"}","proof_exists":true,
 "proof_content":"uid=1000(user) gid=0(root) groups=0(root)\nRCE_CONFIRMED dde4c416676ff815",
 "success":true}

Negative control (1.10.1, bypass closed):

{"role":"followup_fixed","mode":"bypass","token":"e9b8fca5e5acaa53","endpoint":"public_stored",
 "auto_login_status":200,"create_flow_status":201,"flow_id":"ccf86bca-...","exploit_status":400,
 "exploit_body":"{\"detail\":\"This flow cannot be executed.\"}","proof_exists":false,
 "proof_content":null,"success":false}

Environment: Docker 29.6.1; official images langflowai/langflow:1.9.0 and :1.10.1; exploit executed via docker exec inside each container against http://127.0.0.1:7860 (the sandbox cannot reach published host ports). No sanitizers were used; this is a non-sanitized production-path proof. Source identity resolved via image build metadata (pip version) → release tag → git commit: 1.9.0a47f2ad17e (bypass reproduces), 1.10.1a66b75ac26 (bypass closed).

Recommendations / Next Steps

  1. Upgrade to Langflow ≥ 1.10.1, which substitutes the server's trusted code for known component types on the public path and rejects unknown/custom component types carrying code (prepare_public_flow_build). Anyone running 1.9.0–1.10.0 is still exposed to this bypass.
  2. Make public-path code-execution protection independent of the global allow_custom_components flag. The flag governs authenticated behavior; it must not silently disable unauthenticated public-path protection. The allow_public_custom_components opt-in (default false) introduced in v1.10.1 is the correct pattern and should be preserved.
  3. Validate custom-component code at flow storage time (POST /api/v1/flows/ / PATCH) for access_type=PUBLIC flows, so a malicious public flow can never be persisted.
  4. Extend the same hardening to every public graph-build/run entry point (e.g. any future public build or run endpoint), not only the one named in the CVE, and add a regression test that stores a malicious CustomComponent in a public flow, triggers the public build, and asserts the server's trusted code runs instead (assert: no proof side-effect, no attacker code path reached).
  5. Treat LANGFLOW_AUTO_LOGIN=true as incompatible with public-flow exposure: it issues a superuser session with no credentials, which is the precondition that lets an anonymous attacker create the malicious public flow used here.

Additional Notes

  • Bypass vs. alternate trigger: This is a bypass of the CVE's stated fix (1.9.0): it reproduces unauthenticated RCE on the version the CVE says is fixed. It is closed by a later, separate upstream follow-up (1.10.1), which the maintainer's own commit message attributes to the same class of issue ("follow-up to H1-3754930").
  • Trust boundary: Identical to the parent CVE — unauthenticated public build path. The flow-creation pre-step uses the AUTO_LOGIN superuser token (no credentials), exactly as the parent repro does to create its public flow. This is within Langflow's stated threat model (unauthenticated visitors must never execute arbitrary custom-component code on the public path); it is not a documented/accepted behavior.
  • Idempotency: The script removes any prior /tmp/rce-proof at the start of each attempt, uses unique per-attempt tokens, and tears the container down afterwards. Verified by two consecutive full runs (both exit 0, both 2/2 on 1.9.0 and 0/2 on 1.10.1).
  • Not conflated with a separate bug: Same root cause (exec of attacker-controlled custom-component code on the unauthenticated public build path) and same sink (prepare_global_scopeexec) as the parent CVE; only the injection point (request body vs. stored DB data) differs.
  • Edge case considered and ruled out as a different vector (not claimed here): the authenticated POST /api/v1/build/{flow_id}/flow endpoint still accepts a data parameter and, under allow_custom_components=true, its validate_flow_for_current_settings is also a no-op — so an AUTO_LOGIN token holder can RCE via that authenticated path too. That is a different (higher) trust boundary and is by-design per Langflow's threat model ("allow_custom_components grants custom-code execution to authenticated users"); it is therefore not claimed as the variant. The reported variant stays strictly on the unauthenticated public path, matching the parent CVE's trust boundary.

CVE-2026-33017 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:000:49
0:00
session startedaccounts/fireworks/routers/glm-5p2-fast · CVE-2026-33017 · REPRO-20
0:02
0:04
web search
0:05
web search
0:08
0:09
0:11
web search
0:23
0:23
extract_facts
no facts extracted
0:24
0:24
0:24
supportrepro
0:37
0:39
0:39
0:39
0:40
0:40
0:40
0:41
0:42
0:42
0:44
0:44
$cd /data/pruva/project-cache/688af86e-dc4c-4d20-bb02-ccdf8da2c7d0/repo && git log --oneline -5 && echo "---HEAD---" && git rev-parse HEAD && echo "---BRANCH---" && git branch -a 2>/dev/null | head -20 && echo "---TAGS---" && git tag 2>/dev/null | grep -i "v1.8\|v1.9" | head -20
315cc41b43 ci: surface nightly Group 3 stall via 90s pytest thread-timeout (#13938)
ab8508db73 fix(ci): consolidate recent nightly CI fixes
26dc6fd3bc chore: update test durations (#13888)
a277209fe4 ci: push nightly .devX Docker images to langflowai/langflow and fix db-migration-validation image references (#13836)
4da60c4b12 fix: keep nightly prerelease wheels explicit (#13817)
---HEAD---
315cc41b43c446bfb0e63d420ca55021f6f8987e
---BRANCH---
0:44
0:46
0:46
0:46
0:46
$cd /data/pruva/project-cache/688af86e-dc4c-4d20-bb02-ccdf8da2c7d0/repo && git tag 2>/dev/null | grep -E "v1\.8\.1|v1\.9\.0"
v1.8.1
v1.8.1.dev0
v1.8.1.dev1
v1.9.0
v1.9.0.dev0
v1.9.0.dev1
v1.9.0.dev10
v1.9.0.dev11
0:49

Artifacts and Evidence for CVE-2026-33017

Scripts, logs, diffs, and output captured during the reproduction.

bundle/ticket.md0.9 KB
bundle/ticket.json1.4 KB
bundle/repro/repro_attempt.py7.9 KB
bundle/repro/validation_verdict.json0.8 KB
bundle/repro/runtime_manifest.json0.9 KB
bundle/logs/reproduction_steps.log6.5 KB
bundle/logs/container_vuln_1.log3.2 KB
bundle/logs/result_vuln_1.json0.5 KB
bundle/logs/result_vuln_1_stderr.log0.0 KB
bundle/logs/container_vuln_2.log3.2 KB
bundle/logs/result_vuln_2.json0.5 KB
bundle/logs/result_vuln_2_stderr.log0.0 KB
bundle/logs/container_fixed_1.log3.4 KB
bundle/logs/result_fixed_1.json0.4 KB
bundle/logs/result_fixed_1_stderr.log0.0 KB
bundle/logs/container_fixed_2.log3.4 KB
bundle/logs/result_fixed_2.json0.4 KB
bundle/logs/result_fixed_2_stderr.log0.0 KB
bundle/logs/proof_vuln_1.txt0.1 KB
bundle/logs/proof_vuln_2.txt0.1 KB
bundle/logs/vuln_variant/reproduction_steps.log11.8 KB
bundle/logs/vuln_variant/claimed_fixed_image_identity.txt0.4 KB
bundle/logs/vuln_variant/followup_fixed_image_identity.txt0.4 KB
bundle/logs/vuln_variant/container_claimed_fixed_1.log3.0 KB
bundle/logs/vuln_variant/result_claimed_fixed_1.json0.5 KB
bundle/logs/vuln_variant/result_claimed_fixed_1_stderr.log0.0 KB
bundle/logs/vuln_variant/container_claimed_fixed_2.log2.9 KB
bundle/logs/vuln_variant/result_claimed_fixed_2.json0.5 KB
bundle/logs/vuln_variant/result_claimed_fixed_2_stderr.log0.0 KB
bundle/logs/vuln_variant/container_followup_fixed_1.log89.8 KB
bundle/logs/vuln_variant/result_followup_fixed_1.json0.4 KB
bundle/logs/vuln_variant/result_followup_fixed_1_stderr.log0.0 KB
bundle/logs/vuln_variant/container_followup_fixed_2.log89.8 KB
bundle/logs/vuln_variant/result_followup_fixed_2.json0.4 KB
bundle/logs/vuln_variant/result_followup_fixed_2_stderr.log0.0 KB
bundle/logs/vuln_variant/proof_claimed_fixed_1.txt0.1 KB
bundle/logs/vuln_variant/proof_claimed_fixed_2.txt0.1 KB
bundle/logs/vuln_variant/fixed_version.txt0.5 KB
bundle/logs/vuln_variant/claimed_fixed_version.txt0.5 KB
bundle/vuln_variant/variant_attempt.py8.9 KB
bundle/vuln_variant/runtime_manifest.json1.4 KB
bundle/vuln_variant/validation_verdict.json0.9 KB
bundle/vuln_variant/source_identity.json1.9 KB
bundle/vuln_variant/variant_manifest.json5.1 KB
bundle/vuln_variant/patch_analysis.md10.7 KB
bundle/vuln_variant/root_cause_equivalence.json3.3 KB
bundle/repro/reproduction_steps.sh10.1 KB
bundle/repro/rca_report.md10.2 KB
bundle/vuln_variant/reproduction_steps.sh11.7 KB
bundle/vuln_variant/rca_report.md15.2 KB
08 · How to Fix

How to Fix CVE-2026-33017

Upgrade langflow · pip to 1.9.0 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-33017 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-33017

How does the CVE-2026-33017 exploit work?

An attacker creates or targets a public flow, then sends a single POST request to /api/v1/build_public_tmp/{flow_id}/flow with a custom component whose top-level code contains a module-level assignment such as _rce = os.system(...). prepare_global_scope() collects this ast.Assign node and executes it via exec() at graph-build time, yielding arbitrary command execution with the privileges of the Langflow server process, without any authentication.

Which Langflow versions are affected by CVE-2026-33017, and where is it fixed?

Langflow versions <= 1.8.2 (all versions before 1.9.0) are affected. It is fixed in 1.9.0, which hardcodes data=None on the public build endpoint and only loads the stored flow from the database.

How severe is CVE-2026-33017?

It is rated critical severity: an unauthenticated, remote attacker can run arbitrary system commands and read environment variables (including LLM API keys) with a single HTTP request, and it is listed in CISA's Known Exploited Vulnerabilities catalog.

How can I reproduce CVE-2026-33017?

Download the verified script from this page and run it in an isolated environment against a vulnerable Langflow instance (for example langflowai/langflow:1.8.1). It creates a public flow, POSTs a custom component containing an os.system() payload to the public build endpoint, and confirms command execution on the vulnerable build while the same request fails on 1.9.0.
11 · References

References for CVE-2026-33017

Authoritative sources for CVE-2026-33017 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.