CVE-2026-33017: Verified Reproduction
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.
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).
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 — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
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 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00201/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-33017
- 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
JSON body with malicious custom component code (top-level os.system) in POST /api/v1/build_public_tmp/{flow_id}/flow
- /api/v1/build_public_tmp/{flow_id}/flow
- start_flow_build
- build_graph_from_data
- create_class
- prepare_global_scope
- exec
reproduction_steps.sh How the agent worked
Root Cause and Exploit Chain for CVE-2026-33017
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 imagelangflowai/langflow) - Affected versions:
langflow < 1.9.0(reproduction uses1.8.1as the vulnerable image). - Patched versions:
>= 1.9.0(the public build endpoint hardcodesdata=Noneand 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.1container wrote/tmp/rce-proofcontaining the output of theidcommand (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 fixedlangflowai/langflow:1.9.0container 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=data → data=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
- The reproduction is fully automated by
bundle/repro/reproduction_steps.sh(with helperbundle/repro/repro_attempt.py). - The script pulls
langflowai/langflow:1.8.1(vulnerable) andlangflowai/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=trueand--backend-only, - waits for the
/healthendpoint, - performs
GET /api/v1/auto_loginto 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}/flowwith aclient_idcookie and a body whosedatacontains oneCustomComponentnode whosecodeholds a top-level_rce = os.system("id > /tmp/rce-proof && echo RCE_CONFIRMED <token> >> /tmp/rce-proof"), - polls for
/tmp/rce-proofinside the container and copies it out as evidence, then tears the container down.
- starts a fresh Langflow container with
- Expected evidence: on the vulnerable image each attempt produces
logs/proof_vuln_N.txtcontaining theidoutput and the unique token, withexploit_status: 200andproof_exists: trueinlogs/result_vuln_N.json. On the fixed imageproof_exists: falsefor 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 withuid=1000(user)...+ token).bundle/logs/proof_vuln_{1,2}.txt— the proof file exfiltrated from the vulnerable container (idoutput +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.0immediately. 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 enforceaccess_type=PRIVATEby default. - Add an integration test that posts a custom-component payload with a
module-level side-effect sentinel to
build_public_tmpand asserts it never fires, to prevent regressions of this fix.
Additional Notes
- Idempotency: The script removes any prior
/tmp/rce-proofat 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, becauseprepare_global_scope()onlyexec()s nodes it classifies asast.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 theaccess_type=PUBLICrequirement; on the vulnerable path the attacker-supplieddataoverrides the stored definition, so the stored content is irrelevant. - The proof file is written inside the container filesystem and copied out via
docker cpfor durable evidence.
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.
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 -20315cc41b43 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---
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
Artifacts and Evidence for CVE-2026-33017
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-33017
Upgrade langflow · pip to 1.9.0 or later.
FAQ: CVE-2026-33017
How does the CVE-2026-33017 exploit work?
Which Langflow versions are affected by CVE-2026-33017, and where is it fixed?
How severe is CVE-2026-33017?
How can I reproduce CVE-2026-33017?
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.