| 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:
…
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, …)
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.
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.
import os
os.system("id > /tmp/pwned.txt")
class PocComponent(Component):
…
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 endpointLANGFLOW_ALLOW_CUSTOM_COMPONENTS set to false in production unless the feature is genuinely neededgid=0 membership) to reduce lateral-movement / container-escape surface