
管理者専用のターミナルブートストラップルートはログイン状態のみをチェックするため、通常のチームメンバーがCoolifyのリアルタイムターミナルバックエンドを操作し、チームサーバー上でコマンドを実行できるようにします。
管理者専用のターミナルブートストラップルートがログイン状態のみをチェックしていたため、通常のチームメンバーがCoolifyのリアルタイムターミナルバックエンドを操作し、チームサーバー上でコマンドを実行できるようになっていました。
この問題は、オープンソースのセルフホスト型PaaSであるCoolifyをレビュー中に発見しました。その際、非常に直接的なセキュリティ上の問いを念頭に置いていました。
ターミナルアクセスは、実際にバックエンドの信頼境界で強制されているのか、それともUIだけで行われているのか?
今回のケースでは、答えは悪いものでした。
Coolifyはターミナルアクセスをチーム管理者と所有者に制限することを意図していましたが、リアルタイムターミナルのブートストラップルートは、ユーザーがログインしているかどうかのみをチェックしていました。これにより、低権限のチームメンバーがWebSocketターミナルの信頼チェックを満たし、チームサーバー上でのコマンド実行に至ることができました。
私はこの問題を、脆弱性のあるリビジョンから構築したローカルラボでエンドツーエンドで検証し、その後非公開で報告しました。この問題にはCVE-2026-34048が割り当てられ、次のCVSSスコアが付与されました。
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
Coolify: Coolify on GitHub
CVE: CVE-2026-34048
これはオープンソースのセルフホスト型PaaSであるCoolifyに影響します。公式サイトでは、Coolifyは3,641以上のクラウド顧客がいると述べ、Webサイト、データベース、Webアプリケーション、280以上のワンクリックサービスをデプロイするためのプラットフォームとして紹介されています。また、公式v4.0の変更履歴には、数千の企業と個人が1〜2年間、Coolifyを本番環境で使用していると記載されています。
photo0
低権限チームメンバーのセッション -> /terminal/auth および /terminal/auth/ips はログイン状態のみをチェック -> リアルタイムWebSocketがそれらの応答を信頼 -> メンバーがチームサーバーと可視のSSHキーUUIDを列挙 -> /terminal/ws がセッションを受け入れる -> SSHバックアップPTYが生成される -> チームサーバー上でシェルアクセス
Coolifyは、セルフホスト型のPaaSおよびデプロイプラットフォームです。
以下のものを管理します:
最後の機能がここで重要です。
プラットフォームが管理ホストへのターミナルを開くことができるようになると、その認可モデルは単なるアプリケーションロジックではなくなります。 それはインフラストラクチャの信頼境界になります。
重要な質問は、/terminalページが管理者専用に見えるかどうかではありませんでした。
本当の質問は次の通りです。
バックエンドのターミナルパスは、WebSocketセッションが作成される際に、実際に同じ認可境界を強制しているか?
このケースでは、強制されていませんでした。
ターミナル機能は、インフラストラクチャソフトウェアの中でも最も価値の高い表面の一部です。
なぜか?
なぜなら、以下の間のいずれかの不一致が:
通常のアプリケーションユーザーをシェルを使用可能なオペレーターに変える可能性があるからです。
まさにそのため、この表面をテストする価値がありました。
私はランダムなクラッシュや表面的な権限バグを探していたわけではありません。
私はより強力な障害クラスを探していました。
管理者専用の機能が、UIが示唆するよりも弱いバックエンドの信頼チェックに依存していないか?
それが正しい質問でした。
私はCoolifyにアプローチする際、ランダムなエンドポイントをファジングして何か面白いことを期待したわけではありません。
より強力なアプローチは、最初に最もリスクの高い境界を特定することでした。
Coolifyの場合、その境界はターミナルワークフローでした。
これにより、ブートストラップルートが適切な調査場所となりました。
そして、そこに問題がありました。
根本原因は、ターミナルUIとターミナルWebSocketブートストラップルートの間の認可の不一致でした。
脆弱性のあるリビジョンでは:
GET /terminal は can.access.terminal によって保護されていたPOST /terminal/auth は auth()->check() のみチェックしていたPOST /terminal/auth/ips は auth()->check() のみチェックしていたつまり、UIはターミナル認可によってゲートされていたが、バックエンドの信頼境界は単純な認証済みセッションの存在によってゲートされていたのです。
そして、リアルタイムサービスはこれら2つのルートを完全に信頼していました。
docker/coolify-realtime/terminal-server.js において:
verifyClient() は POST /terminal/auth にPOSTしていたPOST /terminal/auth/ips にPOSTしていたそれがバグチェーン全体です。
通常のチームメンバーは、通常のアプリケーション表面から必要な入力を構築できたからです。
/servers は可視のサーバーUUIDを公開していた/server/{uuid} は ip、user、port をレンダリングされたフォームフィールドとして公開していた/security/private-key は可視のチーム秘密鍵UUIDを公開していた/var/www/html/storage/app/ssh/keys/ssh_key@<uuid>
したがって、悪用パスは単純でした:
/terminal/auth を呼び出す/terminal/auth/ips を呼び出す/terminal/ws に接続するこれは理論上の不一致ではありません。 これは実際のバックエンド認可の失敗です。
重要な区別は、バックエンドの信頼とコマンド実行です。
多くのバグは次のように見えます:
それだけでは不十分です。
本当の質問は次の通りです。
低権限のユーザーは、重要なバックエンドの信頼チェックを依然として満たせるか?
このケースでは、答えは「はい」でした。
これは次のようなものではありませんでした:
それは次のようなものでした:
だからこそ、これは本当のセキュリティ問題だったのです。
私はこれを、次のリビジョンから構築した制御されたローカルラボで検証しました。
06f60c9a98bead0c932c6adf7fd43a45d9149048
ラボでは以下を使用しました:
http://127.0.0.1:18000[email protected]localhost -> coolify-testing-host:22 as rootsshws://127.0.0.1:6002/terminal/wsメンバーアカウントは、通常の管理者向けUIを通じてターミナルアクセスを持つことを意図されていませんでした。
これにより、期待されるセキュリティ境界が確立されました。
メンバーセッションを使用して、次のリクエストを送信しました:
POST /terminal/authPOST /terminal/auth/ips両方とも成功しました。
/terminal/auth/ips は、次のターミナル認可ホストを返しました:
coolify-testing-host
host.docker.internal
localhost
127.0.0.1
これにより、バックエンドのブートストラップルートがメンバーセッションを信頼していることが証明されました。
通常の認証済みページから、同じメンバーが以下を列挙できました:
これで、秘密鍵マテリアルの開示を必要とせずにターミナルパスを駆動するのに十分な情報が得られました。
同じ認証済みセッションとXSRFトークンを使用して、次のエンドポイントに接続しました:
ws://127.0.0.1:6002/terminal/ws
ペイロードは、ターミナルバックエンドが期待するのと同じコマンド形式を使用しました:
{"command":["timeout 30 ssh -i /var/www/html/storage/app/ssh/keys/ssh_key@ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o PasswordAuthentication=no -o ConnectTimeout=10 -o ServerAliveInterval=5 -o RequestTTY=no -o LogLevel=ERROR -p '22' 'root'@'coolify-testing-host' 'bash -se' << \\P0C\nprintf '__COOLIFY_POC_BEGIN__\\n'; id; whoami; hostname; printf '__COOLIFY_POC_END__\\n'\nP0C"]}
WebSocketは次のものを返しました:
pty-ready
__COOLIFY_POC_BEGIN__
uid=0(root) gid=0(root) groups=0(root)
root
efa027413801
__COOLIFY_POC_END__
それが重要な証明でした。
単に:
ではなく、管理者専用のターミナルパスを通じて管理ホスト上で実際にコマンドが実行されたことです。
このチェーンの一部だけでも、すでに興味深いものでした。
例えば:
/terminal/auth へのアクセス/terminal/auth/ips へのアクセスしかし、それだけでは却下の余地が残ります。
より強力な検証はエンドツーエンドでした:
これにより、「理論上の認可バグ」と「現実のインフラストラクチャへの影響」の間のギャップが埋まりました。
また、深刻度の弁護がはるかに容易になりました。
この問題はCriticalとして適切に分類されました。
分類は次のとおりです:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
それは理にかなっています。
主張は、未認証の攻撃者が何もないところからシェルアクセスを得られるというものではありません。
主張は次のとおりです:
これは、アプリケーションのRBAC障害から管理ホストへの影響への大きなスコープ変更です。
したがって、権限が低い(ゼロではない)とはいえ、結果は明らかにCriticalです。
一部の人々は、PR:L で始まる脆弱性を過小評価します。
影響を受ける機能がターミナルアクセスである場合、それは間違いです。
本当の質問は次のとおりではありません:
「攻撃者はすでにログインしていたか?」
本当の質問は次のとおりです:
「バックエンドの認可が間違っている場合、その低権限ユーザーは何に到達できるのか?」
このケースでは、答えは次のとおりでした:
これは、通常のメンバー権限バグをはるかに超えています。
最小限の正しい修正は簡単です:
POST /terminal/auth および POST /terminal/auth/ips の両方に can.access.terminal を適用するこれにより、即座の信頼境界の失敗に対処できます。
ローカルの検証パッチでは、これら2つのルートにターミナル認可ミドルウェアを適用することで、メンバーからターミナルへのパスが除去されました。
しかし、より強力な教訓は、バックエンドが攻撃者制御のSSHコマンド文字列をターゲットメタデータの主要ソースとして信頼すべきではないということです。
推奨される強化策は次のとおりです:
これがターミナル機能に必要な種類の是正です:
この問題は、GitHubのセキュリティ報告フローを通じて非公開で報告されました。
報告には以下が含まれていました:
その後、この問題には次のCVEが割り当てられました:
CVE-2026-34048
主な教訓は単純です:
バックエンドのブートストラップチャネルがより弱い状態を信頼している場合、管理者専用のUIは意味をなさない。
それが実際の問題クラスです。
以下の場合、それらはすべて無意味です:
プラットフォームがインフラストラクチャを管理するようになると、認可の不一致は通常のアクセス制御のミスではなくなります。 それらはインフラストラクチャに影響を与える脆弱性になります。
それが本当の教訓です。
この脆弱性は巧妙なペイロードに関するものではありませんでした。
正しい信頼境界を特定することに関するものでした。
Coolifyはターミナルアクセスを管理者専用にすることを意図していました。 しかし、リアルタイムターミナルバックエンドは、ユーザーがログインしているかどうかのみをチェックするルートを信頼していました。
そこから、低権限のチームメンバーがWebSocketターミナルパスを操作し、チームサーバー上でシェル実行に至ることができました。
だからこそ、これがCVE-2026-34048になったのです。