# CVE-2026–17633 - Authenticated RCE in Langflow OSS via /api/v1/custom_component

> Source: <https://dev.to/guidance_white/cve-2026-17633-authenticated-rce-in-langflow-oss-via-apiv1customcomponent-5baj>
> Published: 2026-09-21 23:06:07+00:00

| Field | Value | 
|---|---|
| CVE ID | CVE-2026-17633 | 
| CVSS | 8.5 (HIGH) | 
| CWE | CWE-94 (Improper Control of Generation of Code) | 
| Affected | Langflow OSS 1.0.0 – 1.10.3 | 
| Preconditions | Any authenticated user + `LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true` | 
| Vulnerable endpoint | `POST /api/v1/custom_component` | 

Langflow is an open-source low-code platform for building LLM applications and agent workflows visually. One of its features, **Custom Components**, lets users define a component's behavior directly in Python. That feature is the attack surface for this vulnerability.

IBM's security advisory (published August 5, 2026) disclosed a cluster of issues in Langflow OSS 1.0.0–1.10.3. CVE-2026–17633 is the authenticated RCE reachable through `/api/v1/custom_component`.

From `langflow/api/v1/endpoints.py` (around line 1271):

```
@router.post("/custom_component", status_code=HTTPStatus.OK, include_in_schema=False)
async def custom_component(
 raw_code: CustomComponentRequest,
 user: CurrentActiveUser,
 request: Request,
) -> CustomComponentResponse:
 …
 # The only gate: "is the custom-component feature enabled at all?"
 if not settings.allow_custom_components and not code_hash_matches_any_template(raw_code.code, all_known):
 raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, …)
# scan_code_security() is never called here
 component = Component(_code=effective_code)
 built_frontend_node, component_instance = build_custom_component_template(component, user_id=user.id)
```

The important detail isn't that there's "no validation" - it's that the validation checks the wrong thing. `allow_custom_components` answers "is this user allowed to create custom components," not "is this code's content safe." In production, `LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true` is a common setting, and once it's on, this check passes trivially and the code flows through with zero content inspection.

Langflow does ship a separate AST-based scanner, `scan_code_security()` (covered in section 5), but this endpoint's execution path never calls it.

`prepare_global_scope()`
`custom_component()` calls `build_custom_component_template()`, which flows into `create_class()` in `lfx/custom/validate.py`. That function calls `prepare_global_scope()`, and the submitted code is `exec()`'d shortly after.

``` python
def prepare_global_scope(module):
 exec_globals = globals().copy()
 …
 for node in module.body:
 if isinstance(node, ast.Import | ast.ImportFrom):
 imports.append(node)
 elif isinstance(node, ast.ClassDef | ast.FunctionDef | ast.Assign | ast.AnnAssign):
 definitions.append(node)
 …
 if definitions:
 compiled_code = compile(combined_module, "<string>", "exec")
 exec(compiled_code, exec_globals) # ← exec() happens here
```

This function walks the submitted code's AST and only collects `import` statements and `class`/` def`/assignment nodes into `definitions`. That's the trap.

`os.system(…)` is, at the AST level, an `ast.Expr` node.`isinstance` allow-list above → it's `ClassDef` node - so when the class is defined (i.e., when `exec()` runs), that code executes right along with it.

```
# ❌ Module level - classified as ast.Expr, silently dropped by prepare_global_scope()
import os
os.system("id > /tmp/pwned.txt")
class PocComponent(Component):
 …
# ✅ Inside the class body - part of ClassDef, runs when exec() defines the class
class PocComponent(Component):
 os.system("id > /tmp/pwned.txt") # ← executes at class-definition time
 …
```

So the entire trick an attacker needs is: **put the payload inside the class body, not at module level.** No encoding tricks, no filter bypass gymnastics - just a simple, and simply devastating, design flaw.

The diagram below traces the full path from request to command execution.

In short:

```
Authenticated user (any privilege level)
 │
 ▼
POST /api/v1/custom_component { "code": "<malicious Python class>" }
 │
 ▼
build_custom_component_template() → create_class()
 │
 ▼
prepare_global_scope() - only ClassDef nodes get collected into definitions/exec target
 │
 ▼
compile_class_code() → exec(compiled_class, exec_globals)
 │
 ▼
Class body executes at definition time → RCE achieved
```

No LLM involvement, no scanner to evade. One HTTP request is enough.

Langflow ships a separate AST-based scanner, `scan_code_security()`, in `langflow/agentic/helpers/code_security.py`. But it's wired only into the **Agentic Assistant path** (validating LLM-generated component code) - it's never called from `/api/v1/custom_component` at all.

So CVE-2026–17633 isn't really a scanner bypass; it's exploiting a code path the scanner was never attached to in the first place. The scanner's own detection logic has a separate weakness (missing `vars()` coverage leading to a false `is_safe: True` verdict, tracked as CVE-2026–17632), which surfaced from analyzing the same codebase but is a distinct issue.

`custom_component` endpoint`LANGFLOW_ALLOW_CUSTOM_COMPONENTS` set to `false` in production unless the feature is genuinely needed`gid=0` membership) to reduce lateral-movement / container-escape surface
