GitHub Security Advisory: GHSA-vwmf-pq79-vjvx
Package: langflow (PIP)
Severity: CRITICAL (CVSS 9.8)
CWE: CWE-94: Improper Control of Generation of Code ('Code Injection'), CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection'), CWE-306: Missing Authentication for Critical Function
Vulnerable versions: <= 1.8.2
Patched in: unpatched

Summary: Unauthenticated Remote Code Execution in Langflow via Public Flow Build Endpoint

Details:
## Summary

The `POST /api/v1/build_public_tmp/{flow_id}/flow` endpoint allows building public flows without requiring authentication. When the optional `data` parameter is supplied, the endpoint uses **attacker-controlled flow data** (containing arbitrary Python code in node definitions) instead of the stored flow data from the database. This code is passed to `exec()` with zero sandboxing, resulting in unauthenticated remote code execution.

This is distinct from CVE-2025-3248, which fixed `/api/v1/validate/code` by adding authentication. The `build_public_tmp` endpoint is **designed** to be unauthenticated (for public flows) but incorrectly accepts attacker-supplied flow data containing arbitrary executable code.

## Affected Code

### Vulnerable Endpoint (No Authentication)

**File:** `src/backend/base/langflow/api/v1/chat.py`, lines 580-657

```python
@router.post("/build_public_tmp/{flow_id}/flow")
async def build_public_tmp(
    *,
    flow_id: uuid.UUID,
    data: Annotated[FlowDataRequest | None, Body(embed=True)] = None,  # ATTACKER CONTROLLED
    request: Request,
    # ... NO Depends(get_current_active_user) -- MISSING AUTH ...
):
    """Build a public flow without requiring authentication."""
    client_id = request.cookies.get("client_id")
    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,  # Attacker's data passed directly to graph builder
        current_user=owner_user,
        ...
    )
```

Compare with the authenticated build endpoint at line 138, which requires `current_user: CurrentActiveUser`.

### Code Execution Chain

When attacker-supplied `data` is provided, it flows through:

1. `start_flow_build(data=attacker_data)` → `generate_flow_events()` -- `build.py:81`
2. `create_graph()` → `build_graph_from_data(payload=data.model_dump())` -- `build.py:298`
3. `Graph.from_payload(payload)` parses attacker nodes -- `base.py:1168`
4. `add_nodes_and_edges()` → `initialize()` → `_build_graph()` -- `base.py:270,527`
5. `_instantiate_components_in_vertices()` iterates nodes -- `base.py:1323`
6. `vertex.instantiate_component()` → `instantiate_class(vertex)` -- `loading.py:28`
7. `code = custom_params.pop("code")` extracts attacker code -- `loading.py:43`
8. `eval_custom_component_code(code)` → `create_class(code, class_name)` -- `eval.py:9`
9. `prepare_global_scope(module)` -- `validate.py:323`
10. `exec(compiled_code, exec_globals)` -- **ARBITRARY CODE EXECUTION** -- `validate.py:397`

### Unsandboxed exec() in prepare_global_scope

**File:** `src/lfx/src/lfx/custom/validate.py`, lines 340-397

```python
def prepare_global_scope(module):
    exec_globals = globals().copy()

    # Imports are resolved first (any module can be imported)
    for node in imports:
        module_obj = importlib.import_module(module_name)  # line 352
        exec_globals[variable_name] = module_obj

    # Then ALL to